From ebdfc849421639ab07bc342aaf78ed689b668b14 Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:14:50 -0500 Subject: [PATCH 01/10] fix(protocol-designer): editing labware on top of a module and adapter fixes (#17161) address RQA-3777 RQA-3804 --- .../localization/en/protocol_steps.json | 3 +- .../Designer/DeckSetup/DeckSetupTools.tsx | 56 ++++++++++++++++--- .../__tests__/DeckSetupTools.test.tsx | 4 +- .../Designer/ProtocolSteps/StepSummary.tsx | 22 ++++---- .../src/step-forms/actions/thunks.ts | 55 +++++++++++++++++- 5 files changed, 118 insertions(+), 22 deletions(-) diff --git a/protocol-designer/src/assets/localization/en/protocol_steps.json b/protocol-designer/src/assets/localization/en/protocol_steps.json index 2f498904c61..0ed3f34caf8 100644 --- a/protocol-designer/src/assets/localization/en/protocol_steps.json +++ b/protocol-designer/src/assets/localization/en/protocol_steps.json @@ -143,5 +143,6 @@ "volume_per_well": "Volume per well", "well_name": "Well {{wellName}}", "well_order_title": "{{prefix}} well order", - "well_position": "Well position: X {{x}} Y {{y}} Z {{z}} (mm)" + "well_position": "Well position: X {{x}} Y {{y}} Z {{z}} (mm)", + "unknown_module": "Unknown module" } diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index 48b0ef87bba..d9c2a84a95b 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -33,7 +33,8 @@ import { createDeckFixture, deleteDeckFixture, } from '../../../step-forms/actions/additionalItems' -import { createModule, deleteModule } from '../../../step-forms/actions' +import { deleteModule } from '../../../step-forms/actions' +import { getSavedStepForms } from '../../../step-forms/selectors' import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' import { createContainer, @@ -50,9 +51,12 @@ import { useBlockingHint } from '../../../organisms/BlockingHintModal/useBlockin import { selectors } from '../../../labware-ingred/selectors' import { useKitchen } from '../../../organisms/Kitchen/hooks' import { getDismissedHints } from '../../../tutorial/selectors' -import { createContainerAboveModule } from '../../../step-forms/actions/thunks' -import { ConfirmDeleteStagingAreaModal } from '../../../organisms' +import { + createContainerAboveModule, + createModuleEntityAndChangeForm, +} from '../../../step-forms/actions/thunks' import { BUTTON_LINK_STYLE } from '../../../atoms' +import { ConfirmDeleteStagingAreaModal } from '../../../organisms' import { getSlotInformation } from '../utils' import { ALL_ORDERED_CATEGORIES, FIXTURES, MOAM_MODELS } from './constants' import { LabwareTools } from './LabwareTools' @@ -63,6 +67,13 @@ import type { AddressableAreaName, ModuleModel } from '@opentrons/shared-data' import type { ThunkDispatch } from '../../../types' import type { Fixture } from './constants' +const mapModTypeToStepType: Record = { + heaterShakerModuleType: 'heaterShaker', + magneticModuleType: 'magnet', + temperatureModuleType: 'temperature', + thermocyclerModuleType: 'thermocycler', +} + interface DeckSetupToolsProps { onCloseClick: () => void setHoveredLabware: (defUri: string | null) => void @@ -80,6 +91,7 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { const { makeSnackbar } = useKitchen() const selectedSlotInfo = useSelector(selectors.getZoomedInSlotInfo) const robotType = useSelector(getRobotType) + const savedSteps = useSelector(getSavedStepForms) const [showDeleteLabwareModal, setShowDeleteLabwareModal] = useState< ModuleModel | 'clear' | null >(null) @@ -255,7 +267,11 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { if ( createdLabwareForSlot != null && (!keepExistingLabware || - createdLabwareForSlot.labwareDefURI !== selectedLabwareDefUri) + createdLabwareForSlot.labwareDefURI !== selectedLabwareDefUri || + // if nested labware changes but labware doesn't, still delete both + (createdLabwareForSlot.labwareDefURI === selectedLabwareDefUri && + createdNestedLabwareForSlot?.labwareDefURI !== + selectedNestedLabwareDefUri)) ) { dispatch(deleteContainer({ labwareId: createdLabwareForSlot.id })) } @@ -309,11 +325,33 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { } if (selectedModuleModel != null) { // create module + const moduleType = getModuleType(selectedModuleModel) + + const moduleSteps = Object.values(savedSteps).filter(step => { + return ( + step.stepType === mapModTypeToStepType[moduleType] && + // only update module steps that match the old moduleId + // to accommodate instances of MoaM + step.moduleId === createdModuleForSlot?.id + ) + }) + + const pauseSteps = Object.values(savedSteps).filter(step => { + return ( + step.stepType === 'pause' && + // only update pause steps that match the old moduleId + // to accommodate instances of MoaM + step.moduleId === createdModuleForSlot?.id + ) + }) + dispatch( - createModule({ + createModuleEntityAndChangeForm({ slot, - type: getModuleType(selectedModuleModel), + type: moduleType, model: selectedModuleModel, + moduleSteps, + pauseSteps, }) ) } @@ -344,7 +382,11 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { if ( selectedModuleModel != null && selectedLabwareDefUri != null && - createdLabwareForSlot?.labwareDefURI !== selectedLabwareDefUri + (createdLabwareForSlot?.labwareDefURI !== selectedLabwareDefUri || + // if nested labware changes but labware doesn't, still create both both + (createdLabwareForSlot.labwareDefURI === selectedLabwareDefUri && + createdNestedLabwareForSlot?.labwareDefURI !== + selectedNestedLabwareDefUri)) ) { // create adapter + labware on module dispatch( diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx index 5eab480710e..1c3a7c6a472 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx @@ -10,6 +10,7 @@ import { i18n } from '../../../../assets/localization' import { renderWithProviders } from '../../../../__testing-utils__' import { deleteContainer } from '../../../../labware-ingred/actions' import { deleteModule } from '../../../../step-forms/actions' +import { getSavedStepForms } from '../../../../step-forms/selectors' import { getRobotType } from '../../../../file-data/selectors' import { getEnableAbsorbanceReader } from '../../../../feature-flags/selectors' import { deleteDeckFixture } from '../../../../step-forms/actions/additionalItems' @@ -19,7 +20,6 @@ import { getDeckSetupForActiveItem } from '../../../../top-selectors/labware-loc import { DeckSetupTools } from '../DeckSetupTools' import { LabwareTools } from '../LabwareTools' -import type * as React from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' vi.mock('../LabwareTools') @@ -31,6 +31,7 @@ vi.mock('../../../../step-forms/actions') vi.mock('../../../../step-forms/actions/additionalItems') vi.mock('../../../../labware-ingred/selectors') vi.mock('../../../../tutorial/selectors') +vi.mock('../../../../step-forms/selectors') const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -65,6 +66,7 @@ describe('DeckSetupTools', () => { additionalEquipmentOnDeck: {}, pipettes: {}, }) + vi.mocked(getSavedStepForms).mockReturnValue({}) vi.mocked(getDismissedHints).mockReturnValue([]) }) afterEach(() => { diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx index f97d73e469f..19a648e11b0 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepSummary.tsx @@ -79,6 +79,7 @@ interface StepSummaryProps { export function StepSummary(props: StepSummaryProps): JSX.Element | null { const { currentStep, stepDetails } = props const { t } = useTranslation(['protocol_steps', 'application']) + const unknownModule = t('unkonwn_module') const labwareNicknamesById = useSelector(getLabwareNicknamesById) const additionalEquipmentEntities = useSelector( getAdditionalEquipmentEntities @@ -124,9 +125,8 @@ export function StepSummary(props: StepSummaryProps): JSX.Element | null { engageHeight, magnetAction, } = currentStep - const magneticModuleDisplayName = getModuleDisplayName( - modules[magneticModuleId].model - ) + const magneticModuleDisplayName = + getModuleDisplayName(modules[magneticModuleId]?.model) ?? unknownModule stepSummaryContent = magnetAction === 'engage' ? ( diff --git a/protocol-designer/src/step-forms/actions/thunks.ts b/protocol-designer/src/step-forms/actions/thunks.ts index 0a0f5f17227..65079b0fe2d 100644 --- a/protocol-designer/src/step-forms/actions/thunks.ts +++ b/protocol-designer/src/step-forms/actions/thunks.ts @@ -1,12 +1,21 @@ import { createContainer } from '../../labware-ingred/actions' import { getDeckSetupForActiveItem } from '../../top-selectors/labware-locations' +import { uuid } from '../../utils' +import { changeSavedStepForm } from '../../steplist/actions' -import type { DeckSlotId } from '@opentrons/shared-data' +import type { + DeckSlotId, + ModuleModel, + ModuleType, +} from '@opentrons/shared-data' import type { ThunkAction } from '../../types' import type { CreateContainerAction, RenameLabwareAction, } from '../../labware-ingred/actions' +import type { CreateModuleAction } from './modules' +import type { ChangeSavedStepFormAction } from '../../steplist/actions' +import type { FormData } from '../../form-types' export interface CreateContainerAboveModuleArgs { slot: DeckSlotId @@ -37,3 +46,47 @@ export const createContainerAboveModule: ( }) ) } + +interface ModuleAndChangeFormArgs { + slot: DeckSlotId + type: ModuleType + model: ModuleModel + moduleSteps: FormData[] + pauseSteps: FormData[] +} +export const createModuleEntityAndChangeForm: ( + args: ModuleAndChangeFormArgs +) => ThunkAction = args => ( + dispatch, + getState +) => { + const { slot, model, type, moduleSteps, pauseSteps } = args + const moduleId = `${uuid()}:${type}` + + dispatch({ + type: 'CREATE_MODULE', + payload: { slot, model, type, id: moduleId }, + }) + + // if steps are created with the module that has been regenerated, migrate them to use the correct moduleId + moduleSteps.forEach(step => { + dispatch( + changeSavedStepForm({ + stepId: step.id, + update: { + moduleId, + }, + }) + ) + }) + pauseSteps.forEach(step => { + dispatch( + changeSavedStepForm({ + stepId: step.id, + update: { + moduleId, + }, + }) + ) + }) +} From 20e1dd3810c6eef8386a30a36d69c33c15636d67 Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Mon, 6 Jan 2025 12:29:13 -0500 Subject: [PATCH 02/10] =?UTF-8?q?fix(protocol-designer):=20allow=20minutes?= =?UTF-8?q?Seconds=20timer=20fields=20to=20add=20more=E2=80=A6=20(#17185)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … than 99 minutes closes RQA-3817 --- protocol-designer/src/steplist/fieldLevel/errors.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol-designer/src/steplist/fieldLevel/errors.ts b/protocol-designer/src/steplist/fieldLevel/errors.ts index 7405f097643..892e7f0a11a 100644 --- a/protocol-designer/src/steplist/fieldLevel/errors.ts +++ b/protocol-designer/src/steplist/fieldLevel/errors.ts @@ -34,7 +34,7 @@ export type ErrorChecker = (value: unknown) => string | null export const requiredField: ErrorChecker = (value: unknown) => !value ? FIELD_ERRORS.REQUIRED : null export const isTimeFormat: ErrorChecker = (value: unknown): string | null => { - const timeRegex = new RegExp(/^\d{1,2}:\d{1,2}:\d{1,2}$/g) + const timeRegex = new RegExp(/^\d{1,2}:(?:[0-5]?\d):(?:[0-5]?\d)$/g) return (typeof value === 'string' && timeRegex.test(value)) || !value ? null : FIELD_ERRORS.BAD_TIME_HMS @@ -42,7 +42,7 @@ export const isTimeFormat: ErrorChecker = (value: unknown): string | null => { export const isTimeFormatMinutesSeconds: ErrorChecker = ( value: unknown ): string | null => { - const timeRegex = new RegExp(/^\d{1,2}:\d{1,2}$/g) + const timeRegex = new RegExp(/^\d+:[0-5]?\d$/g) return (typeof value === 'string' && timeRegex.test(value)) || !value ? null : FIELD_ERRORS.BAD_TIME_MS From 7fa13097016205c72efe6b5d011e7624706fda33 Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:33:55 -0500 Subject: [PATCH 03/10] =?UTF-8?q?fix(step-generation):=20configureForVolum?= =?UTF-8?q?e=20volume=20correctly=20set=20to=20am=E2=80=A6=20(#17189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …ount closes RESC-366 Previously, the configure for volume volume was set to the transfer's total volume and not the chunk's volume. But this pr changed it so the volume was set to the chunk number! This pr fixes it so the volume is set to the actual volume --- .../fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json | 2 +- step-generation/src/commandCreators/compound/transfer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json b/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json index 9c6d24abaca..8b8d129418f 100644 --- a/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json +++ b/protocol-designer/fixtures/protocol/8/newAdvancedSettingsAndMultiTemp.json @@ -3624,7 +3624,7 @@ "key": "dd1b37d6-eb8c-4616-bc8a-e1f17556234a", "params": { "pipetteId": "21087f15-4c03-4587-8a2b-1ba0b5a501a0", - "volume": 1 + "volume": 10 } }, { diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index e892dc94b3d..18f99b7e036 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -263,7 +263,7 @@ export const transfer: CommandCreator = ( ? [ curryCommandCreator(configureForVolume, { pipetteId: args.pipette, - volume: chunksPerSubTransfer, + volume: subTransferVol, }), ] : [] From 9bced85d6c314df91cf22c0ad2479facd4e48d5d Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Wed, 8 Jan 2025 12:50:24 -0500 Subject: [PATCH 04/10] fix(protocol-designer): allow custom labware on adapters (#17217) closes RQA-3779 --- .../src/pages/Designer/DeckSetup/LabwareTools.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx index be4f457429e..099416f17c3 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx @@ -430,10 +430,15 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { ) } ) - : getLabwareCompatibleWithAdapter( - loadName - ).map(nestedDefUri => { - const nestedDef = defs[nestedDefUri] + : [ + ...getLabwareCompatibleWithAdapter( + loadName + ), + ...Object.keys(customLabwareDefs), + ].map(nestedDefUri => { + const nestedDef = + defs[nestedDefUri] ?? + customLabwareDefs[nestedDefUri] return ( Date: Thu, 9 Jan 2025 10:49:24 -0500 Subject: [PATCH 05/10] fix(protocol-designer): padding adjustments with forms (#17227) closes RQA-3700 RQA-3693 --- .../src/pages/Designer/DeckSetup/DeckSetupTools.tsx | 6 ++++-- .../Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx | 6 ++++-- .../ProtocolSteps/StepForm/PipetteFields/PositionField.tsx | 1 - .../StepTools/ThermocyclerTools/ThermocyclerState.tsx | 1 - 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index 61b4c73de2f..d95abdfe870 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -36,9 +36,11 @@ import { createDeckFixture, deleteDeckFixture, } from '../../../step-forms/actions/additionalItems' -import { getAdditionalEquipment } from '../../../step-forms/selectors' +import { + getAdditionalEquipment, + getSavedStepForms, +} from '../../../step-forms/selectors' import { deleteModule } from '../../../step-forms/actions' -import { getSavedStepForms } from '../../../step-forms/selectors' import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' import { createContainer, diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx index c3866a806e6..339d0f81695 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx @@ -12,8 +12,10 @@ import { renderWithProviders } from '../../../../__testing-utils__' import { deleteContainer } from '../../../../labware-ingred/actions' import { useKitchen } from '../../../../organisms/Kitchen/hooks' import { deleteModule } from '../../../../step-forms/actions' -import { getAdditionalEquipment } from '../../../../step-forms/selectors' -import { getSavedStepForms } from '../../../../step-forms/selectors' +import { + getAdditionalEquipment, + getSavedStepForms, +} from '../../../../step-forms/selectors' import { getRobotType } from '../../../../file-data/selectors' import { getEnableAbsorbanceReader } from '../../../../feature-flags/selectors' import { deleteDeckFixture } from '../../../../step-forms/actions/additionalItems' diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PositionField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PositionField.tsx index 393e0087f37..681b97be866 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PositionField.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PositionField.tsx @@ -220,7 +220,6 @@ export function PositionField(props: PositionFieldProps): JSX.Element { isIndeterminate={isIndeterminate} units={t('units.millimeter')} id={`TipPositionField_${zName}`} - padding={padding} /> )} 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 b2183d7b678..7f15ecce744 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 @@ -65,7 +65,6 @@ export function ThermocyclerState(props: ThermocyclerStateProps): JSX.Element { flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing4} paddingX={SPACING.spacing16} - paddingTop={SPACING.spacing16} > From 7f3376c890e0dfe26e9a6c622ec2fa74ce22f49a Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 9 Jan 2025 11:55:52 -0500 Subject: [PATCH 06/10] fix(protocol-designer): fix small DQA bugs (#17228) Fixes 3 small style bugs for 1) copy for importing a protocol on PD landing page, 2) background color of selected off deck location, and 3) height of divider in liquid overflow menu Closes RQA-3646, Closes RQA-3672, Closes RQA-3683 --- protocol-designer/cypress/support/commands.ts | 4 ++-- protocol-designer/src/assets/localization/en/shared.json | 1 + protocol-designer/src/pages/Designer/LiquidsOverflowMenu.tsx | 4 ++-- protocol-designer/src/pages/Designer/Offdeck/Offdeck.tsx | 2 +- .../src/pages/Landing/__tests__/Landing.test.tsx | 2 +- protocol-designer/src/pages/Landing/index.tsx | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/protocol-designer/cypress/support/commands.ts b/protocol-designer/cypress/support/commands.ts index 5a40d7762cb..deaa7a4d1ff 100644 --- a/protocol-designer/cypress/support/commands.ts +++ b/protocol-designer/cypress/support/commands.ts @@ -55,7 +55,7 @@ export const locators = { import: 'Import', createNew: 'Create new', createProtocol: 'Create a protocol', - editProtocol: 'Edit existing protocol', + importProtocol: 'Import existing protocol', settingsDataTestid: 'SettingsIconButton', settings: 'Settings', privacyPolicy: 'a[href="https://opentrons.com/privacy-policy"]', @@ -103,7 +103,7 @@ Cypress.Commands.add('verifyCreateNewHeader', () => { Cypress.Commands.add('verifyHomePage', () => { cy.contains(content.welcome) cy.contains('button', locators.createProtocol).should('be.visible') - cy.contains('label', locators.editProtocol).should('be.visible') + cy.contains('label', locators.importProtocol).should('be.visible') cy.getByTestId(locators.settingsDataTestid).should('be.visible') cy.get(locators.privacyPolicy).should('exist').and('be.visible') cy.get(locators.eula).should('exist').and('be.visible') diff --git a/protocol-designer/src/assets/localization/en/shared.json b/protocol-designer/src/assets/localization/en/shared.json index 212ae62bf05..afbd96b1211 100644 --- a/protocol-designer/src/assets/localization/en/shared.json +++ b/protocol-designer/src/assets/localization/en/shared.json @@ -37,6 +37,7 @@ "heatershakermoduletype": "Heater-shaker Module", "hints": "Hints", "import": "Import", + "import_existing_protocol": "Import existing protocol", "incorrect_file_header": "Invalid file type", "incorrect_file_type_body": "Protocol Designer only accepts JSON protocol files created with Protocol Designer. Upload a valid file to continue.", "invalid_json_file_body": "This JSON file is either missing required information or contains sections that Protocol Designer cannot read. At this time we do not support JSON files created outside of Protocol Designer.", diff --git a/protocol-designer/src/pages/Designer/LiquidsOverflowMenu.tsx b/protocol-designer/src/pages/Designer/LiquidsOverflowMenu.tsx index 1a4b2e12c29..38b5867b2cc 100644 --- a/protocol-designer/src/pages/Designer/LiquidsOverflowMenu.tsx +++ b/protocol-designer/src/pages/Designer/LiquidsOverflowMenu.tsx @@ -5,10 +5,10 @@ import { useLocation } from 'react-router-dom' import { ALIGN_CENTER, BORDERS, - Box, COLORS, CURSOR_POINTER, DIRECTION_COLUMN, + Divider, Flex, Icon, LiquidIcon, @@ -92,7 +92,7 @@ export function LiquidsOverflowMenu( ) })} {liquids.length > 0 ? ( - + ) : null} { ) fireEvent.click(screen.getByRole('button', { name: 'Create a protocol' })) expect(vi.mocked(toggleNewProtocolModal)).toHaveBeenCalled() - screen.getByText('Edit existing protocol') + screen.getByText('Import existing protocol') screen.getByRole('img', { name: 'welcome image' }) }) diff --git a/protocol-designer/src/pages/Landing/index.tsx b/protocol-designer/src/pages/Landing/index.tsx index 412800c5bc4..3f2f839e33b 100644 --- a/protocol-designer/src/pages/Landing/index.tsx +++ b/protocol-designer/src/pages/Landing/index.tsx @@ -149,7 +149,7 @@ export function Landing(): JSX.Element { - {t('edit_existing')} + {t('import_existing_protocol')} From 61ef9eb157f5ef6a5926dd9064f6cf8abc01e6a0 Mon Sep 17 00:00:00 2001 From: Max Marrone Date: Thu, 9 Jan 2025 12:36:06 -0500 Subject: [PATCH 07/10] fix(robot-server): Fix `None` ambiguity in labware offset filtering (#17218) --- .../robot_server/labware_offsets/router.py | 28 ++-- .../robot_server/labware_offsets/store.py | 37 ++++-- .../http_api/test_labware_offsets.tavern.yaml | 121 +++++++++++++++++- .../tests/labware_offsets/test_store.py | 33 +---- 4 files changed, 165 insertions(+), 54 deletions(-) diff --git a/robot-server/robot_server/labware_offsets/router.py b/robot-server/robot_server/labware_offsets/router.py index 6aca03e4b18..97b237064a1 100644 --- a/robot-server/robot_server/labware_offsets/router.py +++ b/robot-server/robot_server/labware_offsets/router.py @@ -3,9 +3,11 @@ from datetime import datetime import textwrap -from typing import Annotated, Literal +from typing import Annotated, Literal, Type import fastapi +from pydantic import Json +from pydantic.json_schema import SkipJsonSchema from server_utils.fastapi_utils.light_router import LightRouter from opentrons.protocol_engine import LabwareOffset, LabwareOffsetCreate, ModuleModel @@ -22,7 +24,7 @@ SimpleMultiBody, ) -from .store import LabwareOffsetNotFoundError, LabwareOffsetStore +from .store import DO_NOT_FILTER, LabwareOffsetNotFoundError, LabwareOffsetStore from .fastapi_dependencies import get_labware_offset_store @@ -76,11 +78,11 @@ async def post_labware_offset( # noqa: D103 async def get_labware_offsets( # noqa: D103 store: Annotated[LabwareOffsetStore, fastapi.Depends(get_labware_offset_store)], id: Annotated[ - str | None, + Json[str] | SkipJsonSchema[Type[DO_NOT_FILTER]], fastapi.Query(description="Filter for exact matches on the `id` field."), - ] = None, + ] = DO_NOT_FILTER, definition_uri: Annotated[ - str | None, + Json[str] | SkipJsonSchema[Type[DO_NOT_FILTER]], fastapi.Query( alias="definitionUri", description=( @@ -88,23 +90,23 @@ async def get_labware_offsets( # noqa: D103 " (Not to be confused with `location.definitionUri`.)" ), ), - ] = None, + ] = DO_NOT_FILTER, location_slot_name: Annotated[ - DeckSlotName | None, + Json[DeckSlotName] | SkipJsonSchema[Type[DO_NOT_FILTER]], fastapi.Query( alias="locationSlotName", description="Filter for exact matches on the `location.slotName` field.", ), - ] = None, + ] = DO_NOT_FILTER, location_module_model: Annotated[ - ModuleModel | None, + Json[ModuleModel | None] | SkipJsonSchema[Type[DO_NOT_FILTER]], fastapi.Query( alias="locationModuleModel", description="Filter for exact matches on the `location.moduleModel` field.", ), - ] = None, + ] = DO_NOT_FILTER, location_definition_uri: Annotated[ - str | None, + Json[str | None] | SkipJsonSchema[Type[DO_NOT_FILTER]], fastapi.Query( alias="locationDefinitionUri", description=( @@ -112,9 +114,9 @@ async def get_labware_offsets( # noqa: D103 " (Not to be confused with just `definitionUri`.)" ), ), - ] = None, + ] = DO_NOT_FILTER, cursor: Annotated[ - int | None, + int | SkipJsonSchema[None], fastapi.Query( description=( "The first index to return out of the overall filtered result list." diff --git a/robot-server/robot_server/labware_offsets/store.py b/robot-server/robot_server/labware_offsets/store.py index 9f7e76cfd57..93968a30986 100644 --- a/robot-server/robot_server/labware_offsets/store.py +++ b/robot-server/robot_server/labware_offsets/store.py @@ -1,9 +1,21 @@ # noqa: D100 +from typing import Type + from opentrons.protocol_engine import LabwareOffset, ModuleModel from opentrons.types import DeckSlotName +class DO_NOT_FILTER: + """A sentinel value for when a filter should not be applied. + + This is different from filtering on `None`, which returns only entries where the + value is equal to `None`. + """ + + pass + + # todo(mm, 2024-12-06): Convert to be SQL-based and persistent instead of in-memory. # https://opentrons.atlassian.net/browse/EXEC-1015 class LabwareOffsetStore: @@ -19,11 +31,15 @@ def add(self, offset: LabwareOffset) -> None: def search( self, - id_filter: str | None, - definition_uri_filter: str | None, - location_slot_name_filter: DeckSlotName | None, - location_module_model_filter: ModuleModel | None, - location_definition_uri_filter: str | None, + id_filter: str | Type[DO_NOT_FILTER] = DO_NOT_FILTER, + definition_uri_filter: str | Type[DO_NOT_FILTER] = DO_NOT_FILTER, + location_slot_name_filter: DeckSlotName | Type[DO_NOT_FILTER] = DO_NOT_FILTER, + location_module_model_filter: ModuleModel + | None + | Type[DO_NOT_FILTER] = DO_NOT_FILTER, + location_definition_uri_filter: str + | None + | Type[DO_NOT_FILTER] = DO_NOT_FILTER, # todo(mm, 2024-12-06): Support pagination (cursor & pageLength query params). # The logic for that is currently duplicated across several places in # robot-server and api. We should try to clean that up, or at least avoid @@ -33,13 +49,14 @@ def search( def is_match(candidate: LabwareOffset) -> bool: return ( - id_filter in (None, candidate.id) - and definition_uri_filter in (None, candidate.definitionUri) - and location_slot_name_filter in (None, candidate.location.slotName) + id_filter in (DO_NOT_FILTER, candidate.id) + and definition_uri_filter in (DO_NOT_FILTER, candidate.definitionUri) + and location_slot_name_filter + in (DO_NOT_FILTER, candidate.location.slotName) and location_module_model_filter - in (None, candidate.location.moduleModel) + in (DO_NOT_FILTER, candidate.location.moduleModel) and location_definition_uri_filter - in (None, candidate.location.definitionUri) + in (DO_NOT_FILTER, candidate.location.definitionUri) ) return [ diff --git a/robot-server/tests/integration/http_api/test_labware_offsets.tavern.yaml b/robot-server/tests/integration/http_api/test_labware_offsets.tavern.yaml index d9ff35d7136..f84e5b15d56 100644 --- a/robot-server/tests/integration/http_api/test_labware_offsets.tavern.yaml +++ b/robot-server/tests/integration/http_api/test_labware_offsets.tavern.yaml @@ -83,7 +83,7 @@ stages: # Just a basic test here. More complicated tests for the filters belong in the unit tests. - name: Test getting labware offsets with a filter request: - url: '{ot3_server_base_url}/labwareOffsets?locationSlotName=A2' + url: '{ot3_server_base_url}/labwareOffsets?locationSlotName="A2"' method: GET response: json: @@ -129,3 +129,122 @@ stages: meta: cursor: 0 totalLength: 0 + +--- +# Some of the filter query parameters can have `null` values or be omitted, +# with different semantics between the two. That distinction takes a bit of care to +# preserve across our code, so here we test it specifically. +test_name: Test null vs. omitted filter query parameters +marks: + - usefixtures: + - ot3_server_base_url +stages: + - name: POST test offset 1 + request: + method: POST + url: '{ot3_server_base_url}/labwareOffsets' + json: + data: + definitionUri: testNamespace/loadName1/1 + location: + slotName: A1 + # No moduleModel + # No definitionUri + vector: + x: 1 + y: 2 + z: 3 + response: + status_code: 201 + save: + json: + offset_1_data: data + - name: POST test offset 2 + request: + method: POST + url: '{ot3_server_base_url}/labwareOffsets' + json: + data: + definitionUri: testNamespace/loadName2/1 + location: + slotName: A1 + moduleModel: temperatureModuleV2 + # No definitionUri + vector: + x: 1 + y: 2 + z: 3 + response: + status_code: 201 + save: + json: + offset_2_data: data + - name: POST test offset 3 + request: + method: POST + url: '{ot3_server_base_url}/labwareOffsets' + json: + data: + definitionUri: testNamespace/loadName2/1 + location: + slotName: A1 + # no moduleModel + definitionUri: testNamespace/adapterLoadName/1 + vector: + x: 1 + y: 2 + z: 3 + response: + status_code: 201 + save: + json: + offset_3_data: data + - name: POST test offset 4 + request: + method: POST + url: '{ot3_server_base_url}/labwareOffsets' + json: + data: + definitionUri: testNamespace/loadName3/1 + location: + slotName: A1 + moduleModel: temperatureModuleV2 + definitionUri: testNamespace/adapterLoadName/1 + vector: + x: 1 + y: 2 + z: 3 + response: + status_code: 201 + save: + json: + offset_4_data: data + - name: Test no filters + request: + url: '{ot3_server_base_url}/labwareOffsets' + response: + json: + data: + - !force_format_include '{offset_1_data}' + - !force_format_include '{offset_2_data}' + - !force_format_include '{offset_3_data}' + - !force_format_include '{offset_4_data}' + meta: !anydict + - name: Test filtering on locationModuleModel=null + request: + url: '{ot3_server_base_url}/labwareOffsets?locationModuleModel=null' + response: + json: + data: + - !force_format_include '{offset_1_data}' + - !force_format_include '{offset_3_data}' + meta: !anydict + - name: Test filtering on locationDefinitionUri=null + request: + url: '{ot3_server_base_url}/labwareOffsets?locationDefinitionUri=null' + response: + json: + data: + - !force_format_include '{offset_1_data}' + - !force_format_include '{offset_2_data}' + meta: !anydict diff --git a/robot-server/tests/labware_offsets/test_store.py b/robot-server/tests/labware_offsets/test_store.py index 0f28f2e6825..9f122e2930a 100644 --- a/robot-server/tests/labware_offsets/test_store.py +++ b/robot-server/tests/labware_offsets/test_store.py @@ -16,13 +16,7 @@ def _get_all(store: LabwareOffsetStore) -> list[LabwareOffset]: - return store.search( - id_filter=None, - definition_uri_filter=None, - location_definition_uri_filter=None, - location_module_model_filter=None, - location_slot_name_filter=None, - ) + return store.search() def test_filters() -> None: @@ -52,25 +46,10 @@ def test_filters() -> None: subject.add(labware_offset) # No filters: - assert ( - subject.search( - id_filter=None, - definition_uri_filter=None, - location_definition_uri_filter=None, - location_module_model_filter=None, - location_slot_name_filter=None, - ) - == labware_offsets - ) + assert subject.search() == labware_offsets # Filter on one thing: - result = subject.search( - id_filter=None, - definition_uri_filter="definition-uri-b", - location_definition_uri_filter=None, - location_module_model_filter=None, - location_slot_name_filter=None, - ) + result = subject.search(definition_uri_filter="definition-uri-b") assert len(result) == 3 assert result == [ entry for entry in labware_offsets if entry.definitionUri == "definition-uri-b" @@ -80,9 +59,6 @@ def test_filters() -> None: result = subject.search( id_filter="id-2", definition_uri_filter="definition-uri-b", - location_definition_uri_filter=None, - location_module_model_filter=None, - location_slot_name_filter=None, ) assert result == [labware_offsets[1]] @@ -90,9 +66,6 @@ def test_filters() -> None: result = subject.search( id_filter="id-1", definition_uri_filter="definition-uri-b", - location_definition_uri_filter=None, - location_module_model_filter=None, - location_slot_name_filter=None, ) assert result == [] From 6db70abaff3215c66cf92f5693d41bc4f830eec9 Mon Sep 17 00:00:00 2001 From: alexjoel42 Date: Thu, 9 Jan 2025 13:12:19 -0500 Subject: [PATCH 08/10] feat(protocol-designer): Make createNew.ts more robust and use it to start testing protocol actions in PD (#17156) # Overview These additions to the test case of transferSetting.js.cy are the start of providing greater test coverage to our transfer form. At this moment, it only goes up to saving a liquid and then opening the transfer form. ## Test Plan and Hands on Testing These are changes exclusively to cypress. So it will be tested when folks run their code. ## Changelog 1. opentrons/protocol-designer/cypress/e2e/transferSettings.cy.js . 2. opentrons/protocol-designer/cypress/support/commands.ts added some additional helper functions Please add special attention to checking if this was the correct way to add liquid. Saving it took a bit of extra effort. AFAIK > When you submit a form, the browser sends a POST or GET request to the action URL specified in the
tag or the current page URL if no action is defined. Cypress executes the test and observes the page reload, which can interfere with your test flow if you're not handling it properly. So I tried to do the following ```Javascript cy.contains('button', 'Add liquid').click() cy.contains('button', 'Liquid').click() cy.contains('button', 'Define a liquid').click() cy.get('input[name="name"]') // Select the input with name="name" .type('My liquid!') cy.get('div[aria-label="ModalShell_ModalArea"]') .find('form') // Target the form that wraps the button .invoke('submit', (e) => { e.preventDefault(); // Prevent default behavior }); ``` ## Review requests @y3rsh @jerader ## Risk assessment Medium. This might take some extra time with developers to ensure that I'm not breaking existing testing behavior. --- protocol-designer/cypress/e2e/createNew.cy.ts | 13 +- .../cypress/e2e/createNewFlex.cy.ts | 72 ++++ protocol-designer/cypress/e2e/home.cy.ts | 1 + .../cypress/e2e/mixSettings.cy.js | 299 -------------- .../cypress/e2e/transferSettings.cy.js | 347 ---------------- .../cypress/e2e/transferSettings.cy.ts | 77 ++++ protocol-designer/cypress/support/commands.ts | 25 +- .../cypress/support/createNew.ts | 382 +++++++++++++++++- 8 files changed, 551 insertions(+), 665 deletions(-) create mode 100644 protocol-designer/cypress/e2e/createNewFlex.cy.ts delete mode 100644 protocol-designer/cypress/e2e/mixSettings.cy.js delete mode 100644 protocol-designer/cypress/e2e/transferSettings.cy.js create mode 100644 protocol-designer/cypress/e2e/transferSettings.cy.ts diff --git a/protocol-designer/cypress/e2e/createNew.cy.ts b/protocol-designer/cypress/e2e/createNew.cy.ts index fb1c70470b3..230721febad 100644 --- a/protocol-designer/cypress/e2e/createNew.cy.ts +++ b/protocol-designer/cypress/e2e/createNew.cy.ts @@ -1,21 +1,16 @@ -import { - Actions, - Verifications, - runCreateTest, - verifyCreateProtocolPage, -} from '../support/createNew' +import { Actions, Verifications, runCreateTest } from '../support/createNew' import { UniversalActions } from '../support/universalActions' +import '../support/commands' describe('The Redesigned Create Protocol Landing Page', () => { beforeEach(() => { cy.visit('/') + cy.closeAnalyticsModal() }) it('content and step 1 flow works', () => { - cy.closeAnalyticsModal() - cy.clickCreateNew() cy.verifyCreateNewHeader() - verifyCreateProtocolPage() + cy.clickCreateNew() const steps: Array = [ Verifications.OnStep1, Verifications.FlexSelected, diff --git a/protocol-designer/cypress/e2e/createNewFlex.cy.ts b/protocol-designer/cypress/e2e/createNewFlex.cy.ts new file mode 100644 index 00000000000..8f9017e63ae --- /dev/null +++ b/protocol-designer/cypress/e2e/createNewFlex.cy.ts @@ -0,0 +1,72 @@ +import { Actions, Verifications, runCreateTest } from '../support/createNew' +import { UniversalActions } from '../support/universalActions' + +describe('The Redesigned Create Protocol Landing Page', () => { + beforeEach(() => { + cy.visit('/') + cy.closeAnalyticsModal() + }) + + it('content and step 1 flow works', () => { + cy.clickCreateNew() + cy.verifyCreateNewHeader() + const steps: Array = [ + Verifications.OnStep1, + Verifications.FlexSelected, + UniversalActions.Snapshot, + Actions.SelectOT2, + Verifications.OT2Selected, + UniversalActions.Snapshot, + Actions.SelectFlex, + Verifications.FlexSelected, + UniversalActions.Snapshot, + Actions.Confirm, + Verifications.OnStep2, + Actions.SingleChannelPipette50, + Verifications.StepTwo50uL, + UniversalActions.Snapshot, + Actions.Confirm, + Verifications.StepTwoPart3, + UniversalActions.Snapshot, + Actions.Confirm, + Verifications.OnStep3, + Actions.YesGripper, + Actions.Confirm, + Verifications.Step4Verification, + Actions.AddThermocycler, + Verifications.ThermocyclerImg, + Actions.AddHeaterShaker, + Verifications.HeaterShakerImg, + Actions.AddMagBlock, + Verifications.MagBlockImg, + Actions.AddTempdeck2, + Verifications.Tempdeck2Img, + Actions.Confirm, + Actions.Confirm, + Actions.Confirm, + Actions.EditProtocolA, + Actions.ChoseDeckSlotC2, + Actions.AddHardwareLabware, + Actions.ClickLabwareHeader, + Actions.ClickWellPlatesSection, + Actions.SelectArmadillo96WellPlate, + Actions.ChoseDeckSlotC2Labware, + Actions.AddLiquid, + Actions.ClickLiquidButton, + Actions.DefineLiquid, + Actions.LiquidSaveWIP, + Actions.WellSelector, + Actions.LiquidDropdown, + Verifications.LiquidPage, + UniversalActions.Snapshot, + Actions.SelectLiquidWells, + Actions.SetVolumeAndSaveforWells, + Actions.ChoseDeckSlotC3, + Actions.AddHardwareLabware, + Actions.ClickLabwareHeader, + Actions.ClickWellPlatesSection, + Actions.SelectBioRad96WellPlate, + ] + runCreateTest(steps) + }) +}) diff --git a/protocol-designer/cypress/e2e/home.cy.ts b/protocol-designer/cypress/e2e/home.cy.ts index 211100898bf..32a7c766d8e 100644 --- a/protocol-designer/cypress/e2e/home.cy.ts +++ b/protocol-designer/cypress/e2e/home.cy.ts @@ -1,6 +1,7 @@ describe('The Home Page', () => { beforeEach(() => { cy.visit('/') + cy.closeAnalyticsModal() }) it('successfully loads', () => { diff --git a/protocol-designer/cypress/e2e/mixSettings.cy.js b/protocol-designer/cypress/e2e/mixSettings.cy.js deleted file mode 100644 index 600d25971be..00000000000 --- a/protocol-designer/cypress/e2e/mixSettings.cy.js +++ /dev/null @@ -1,299 +0,0 @@ -// TODO: refactor to test with new navigation -// const isMacOSX = Cypress.platform === 'darwin' -// const invalidInput = 'abcdefghijklmnopqrstuvwxyz!@#$%^&*()<>?,-' -// const batchEditClickOptions = { [isMacOSX ? 'metaKey' : 'ctrlKey']: true } - -// function importProtocol() { -// cy.fixture('../../fixtures/protocol/5/mixSettings.json').then(fileContent => { -// cy.get('input[type=file]').upload({ -// fileContent: JSON.stringify(fileContent), -// fileName: 'fixture.json', -// mimeType: 'application/json', -// encoding: 'utf8', -// }) -// cy.get('div') -// .contains( -// 'Your protocol will be automatically updated to the latest version.' -// ) -// .should('exist') -// cy.get('button').contains('ok', { matchCase: false }).click() -// // wait until computation is done before proceeding, with generous timeout -// cy.get('[data-test="ComputingSpinner"]', { timeout: 30000 }).should( -// 'not.exist' -// ) -// }) -// } - -// function openDesignTab() { -// cy.get('button[id=NavTab_design]').click() -// cy.get('button').contains('ok').click() - -// // Verify the Design Page -// cy.get('#TitleBar_main > h1').contains('Multi select banner test protocol') -// cy.get('#TitleBar_main > h2').contains('STARTING DECK STATE') -// cy.get('button[id=StepCreationButton]').contains('+ Add Step') -// } - -// function enterBatchEdit() { -// cy.get('[data-test="StepItem_1"]').click(batchEditClickOptions) -// cy.get('button').contains('exit batch edit').should('exist') -// } - -describe('Advanced Settings for Mix Form', () => { - // before(() => { - // cy.visit('/') - // cy.closeAnnouncementModal() - // importProtocol() - // openDesignTab() - // }) - // it('should verify the batch edit form works as expected', () => { - // // Verify functionality of mix settings with different labware - // enterBatchEdit() - // // Different labware disbales aspirate and dispense Flowrate , tipPosition, delay and touchTip - // // step 4 has different labware than step 1 - // cy.get('[data-test="StepItem_4"]').click(batchEditClickOptions) - // // well-order is always enabled - // cy.get('[id=WellOrderField_button_mix]').should('be.visible') - // // Aspirate Flowrate disabled - // cy.get('input[name="aspirate_flowRate"]').should('be.disabled') - // // TipPosition Aspirate should be disabled - // cy.get('[id=TipPositionIcon_mix_mmFromBottom]').should('not.be.enabled') - // // Dispense Flowrate disbled - // cy.get('input[name="dispense_flowRate"]').should('be.disabled') - // // Delay aspirate & dispense and Touch tip is disabled - // cy.get('input[name="aspirate_delay_checkbox"]').should('be.disabled') - // cy.get('input[name="dispense_delay_checkbox"]').should('be.disabled') - // cy.get('input[name="mix_touchTip_checkbox"]').should('be.disabled') - // // Save button is disabled - // cy.get('button').contains('save').should('be.disabled') - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Verify functionality of mix settings with same labware - // enterBatchEdit() - // // Same labware enables aspirate and dispense Flowrate ,tipPosition ,delay and touchTip - // // deslecting step 4 - // cy.get('[data-test="StepItem_4"]').click(batchEditClickOptions) - // // step 2 has same labware as step 1 - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // Aspirate Flowrate are enabled - // cy.get('input[name="aspirate_flowRate"]').should('be.enabled') - // // Dispense Flowrate are enabled - // cy.get('input[name="dispense_flowRate"]').should('be.enabled') - // // TipPosition Aspirate should be enabled - // cy.get('[id=TipPositionIcon_mix_mmFromBottom]').should('not.be.disabled') - // // Delay in aspirate and Dispense settings is enabled - // cy.get('input[name="aspirate_delay_checkbox"]').should('be.enabled') - // cy.get('input[name="dispense_delay_checkbox"]').should('be.enabled') - // // Touchtip in Dispense settings is enabled - // cy.get('input[name="mix_touchTip_checkbox"]').should('be.enabled') - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Verify invalid input in delay field - // // click on step 2 in batch edit mode - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // cy.get('input[name="aspirate_delay_checkbox"]').click({ force: true }) - // cy.get('input[name="aspirate_delay_seconds"]') - // .type(invalidInput) - // .should('be.empty') - // // click on Discard Changes button to not save the changes - // cy.get('button').contains('discard changes').click() - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Verify indeterminate state of flowrate - // // click on step 2 in batch edit mode - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // cy.get('input[name="aspirate_flowRate"]').click({ force: true }) - // cy.contains( - // 'The default P1000 Single-Channel GEN2 flow rate is optimal for handling aqueous liquids' - // ) - // cy.get('input[name="aspirate_flowRate_customFlowRate"]').type('100') - // cy.get('button').contains('Done').click() - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Click on step 1 as it has flowrate set to 100 from previous testcase - // cy.get('[data-test="StepItem_1"]').click(batchEditClickOptions) - // // indeterminate state in flowrate is empty - // cy.get('input[name="aspirate_flowRate"]').should('have.value', '') - // // Verify functionality of flowrate in batch edit mix form - // // Batch editing the Flowrate value - // cy.get('input[name="aspirate_flowRate"]').click({ force: true }) - // cy.contains( - // 'The default P1000 Single-Channel GEN2 flow rate is optimal for handling aqueous liquids' - // ) - // cy.get('input[name="aspirate_flowRate_customFlowRate"]').type('100') - // cy.get('button').contains('Done').click() - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Click on step 2 to verify that flowrate is updated to 100 - // cy.get('[data-test="StepItem_2"]').click() - // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click() - // // Verify that flowrate value - // cy.get('input[name="aspirate_flowRate"]').should('have.value', 100) - // // Click on step 1 to verify that flowrate is updated to 100 - // cy.get('[data-test="StepItem_1"]').click() - // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click() - // // Verify that flowrate value - // cy.get('input[name="aspirate_flowRate"]').should('have.value', 100) - // // Verify delay settings indeterminate value - // // Click on step 2, to enter batch edit mode - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // Select delay settings - // cy.get('input[name="aspirate_delay_checkbox"]') - // .check({ force: true }) - // .should('be.checked') - // cy.get('input[name="aspirate_delay_seconds"]').type('2') - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Click on step 3 to generate indertminate state for delay settings. - // cy.get('[data-test="StepItem_3"]').click(batchEditClickOptions) - // // Verify the tooltip here - // cy.contains('delay').trigger('pointerover') - // cy.get('div[role="tooltip"]').should( - // 'contain', - // 'Not all selected steps are using this setting' - // ) - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Verify delay settings batch editing in mix form - // // Click on step 1, to enter batch edit mode - // cy.get('[data-test="StepItem_1"]').click(batchEditClickOptions) - // // Click on step 2 to batch edit mix settings - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // Select delay settings - // cy.get('input[name="aspirate_delay_checkbox"]').click({ force: true }) - // cy.get('input[name="aspirate_delay_seconds"]').clear().type('2') - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Click on step 1 to verify that delay has volume set to 2 - // cy.get('[data-test="StepItem_1"]').click() - // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click() - // // Verify that volume is set to 2 - // cy.get('input[name="aspirate_delay_seconds"]').should('have.value', 2) - // // Click on step 2 to verify that delay has volume set to 2 - // cy.get('[data-test="StepItem_2"]').click() - // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click() - // // Verify that volume is set to 2 - // cy.get('input[name="aspirate_delay_seconds"]').should('have.value', 2) - // // Verify touchTip settings indeterminate value - // cy.get('[data-test="StepItem_2"]').click() - // // Click on step 2, to enter batch edit mode - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // Select touchTip settings - // cy.get('input[name="mix_touchTip_checkbox"]').click({ force: true }) - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Click on step 1 to generate indertminate state for touchTip settings. - // cy.get('[data-test="StepItem_1"]').click(batchEditClickOptions) - // // Verify the tooltip here - // cy.contains('touch tip').trigger('pointerover') - // cy.get('div[role="tooltip"]').should( - // 'contain', - // 'Not all selected steps are using this setting' - // ) - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Verify touchTip settings batch editing in mix form - // cy.get('[data-test="StepItem_2"]').click() - // // Click on step 2, to enter batch edit mode - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // Click on step 3 to batch edit mix settings - // cy.get('[data-test="StepItem_3"]').click(batchEditClickOptions) - // // Select touchTip settings - // cy.get('input[name="mix_touchTip_checkbox"]').click({ force: true }) - // // cy.get('[id=TipPositionField_mix_touchTip_mmFromBottom]').type('24') - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Click on step 2 to verify that touchTip has volume set to 2 - // cy.get('[data-test="StepItem_2"]').click() - // cy.get('button[id="AspDispSection_settings_button_dispense"]').click() - // // Verify that volume is set - // cy.get('[id=TipPositionField_mix_touchTip_mmFromBottom]').should( - // 'have.value', - // 16.4 - // ) - // // Click on step 1 to verify that touchTip has volume set - // cy.get('[data-test="StepItem_3"]').click() - // cy.get('button[id="AspDispSection_settings_button_dispense"]').click() - // // Verify that volume is set - // cy.get('[id=TipPositionField_mix_touchTip_mmFromBottom]').should( - // 'have.value', - // 16.4 - // ) - // // Verify blowout settings indeterminate value - // // Click on step 2, to enter batch edit mode - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // Select blowout settings - // cy.get('input[name="blowout_checkbox"]').click({ force: true }) - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Click on step 1 to generate indertminate state for blowout settings. - // cy.get('[data-test="StepItem_1"]').click(batchEditClickOptions) - // // Verify the tooltip here - // cy.contains('blowout').trigger('pointerover') - // cy.get('div[role="tooltip"]').should( - // 'contain', - // 'Not all selected steps are using this setting' - // ) - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Verify blowout settings batch editing in mix form - // // Click on step 2, to enter batch edit mode - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // Click on step 3 to batch edit mix settings - // cy.get('[data-test="StepItem_3"]').click(batchEditClickOptions) - // // Select blowout settings - // cy.get('input[name="blowout_checkbox"]').click({ force: true }) - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Click on step 2 to verify that blowout has dest well selected - // cy.get('[data-test="StepItem_2"]').click() - // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click() - // // Verify dest well is selected - // cy.get('[id=BlowoutLocationField_dropdown]').should($input => { - // const value = $input.val() - // const expectedSubstring = 'trashBin' - // expect(value).to.include(expectedSubstring) - // }) - // // Click on step 3 to verify the batch editing - // cy.get('[data-test="StepItem_3"]').click() - // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click() - // // Verify that dest well is selected for the blowout option - // cy.get('[id=BlowoutLocationField_dropdown]').should($input => { - // const value = $input.val() - // const expectedSubstring = 'trashBin' - // expect(value).to.include(expectedSubstring) - // }) - // // verify well-order indeterminate state - // // Click on step 2, to enter batch edit and click on well order to change the order - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // click on well-order and change the order - // cy.get('[id=WellOrderField_button_mix]').click({ force: true }) - // cy.get('h4').contains('Well Order') - // cy.get('p').contains( - // 'Change the order in which the robot aspirates from the selected wells' - // ) - // cy.get('select[name="mix_wellOrder_first"]') - // .select('Bottom to top') - // .should('have.value', 'b2t') - // cy.get('select[name="mix_wellOrder_second"]') - // .select('Left to right') - // .should('have.value', 'l2r') - // // Click done button to save the changes - // cy.get('button').contains('done').click() - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Click on step 1, as it has different well order - // cy.get('[data-test="StepItem_1"]').click(batchEditClickOptions) - // cy.get('[id=WellOrderField_button_mix]').contains('mixed') - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // }) -}) diff --git a/protocol-designer/cypress/e2e/transferSettings.cy.js b/protocol-designer/cypress/e2e/transferSettings.cy.js deleted file mode 100644 index 6fded2dd9e6..00000000000 --- a/protocol-designer/cypress/e2e/transferSettings.cy.js +++ /dev/null @@ -1,347 +0,0 @@ -// TODO: refactor to test with new navigation -// const isMacOSX = Cypress.platform === 'darwin' -// const batchEditClickOptions = { [isMacOSX ? 'metaKey' : 'ctrlKey']: true } - -// const invalidInput = 'abcdefghijklmnopqrstuvwxyz!@#$%^&*()<>?,-' - -// function importProtocol() { -// cy.fixture('../../fixtures/protocol/5/transferSettings.json').then( -// fileContent => { -// cy.get('input[type=file]').upload({ -// fileContent: JSON.stringify(fileContent), -// fileName: 'fixture.json', -// mimeType: 'application/json', -// encoding: 'utf8', -// }) -// cy.get('[data-test="ComputingSpinner"]').should('exist') -// cy.get('div') -// .contains( -// 'Your protocol will be automatically updated to the latest version.' -// ) -// .should('exist') -// cy.get('button').contains('ok', { matchCase: false }).click() -// // wait until computation is done before proceeding, with generous timeout -// cy.get('[data-test="ComputingSpinner"]', { timeout: 30000 }).should( -// 'not.exist' -// ) -// } -// ) -// } - -// function openDesignTab() { -// cy.get('button[id=NavTab_design]').click() -// cy.get('button').contains('ok').click() - -// // Verify the Design Page -// cy.get('#TitleBar_main > h1').contains('Multi select banner test protocol') -// cy.get('#TitleBar_main > h2').contains('STARTING DECK STATE') -// cy.get('button[id=StepCreationButton]').contains('+ Add Step') -// } - -// function enterBatchEdit() { -// cy.get('[data-test="StepItem_1"]').click(batchEditClickOptions) -// cy.get('button').contains('exit batch edit').should('exist') -// } - -describe('Advanced Settings for Transfer Form', () => { - // before(() => { - // cy.visit('/') - // cy.closeAnnouncementModal() - // importProtocol() - // openDesignTab() - // }) - // it('Verify functionality of the transfer form', () => { - // // Verify functionality of advanced settings with different pipette and labware - // enterBatchEdit() - // // Different Pipette disables aspirate and dispense Flowrate and Mix settings - // // step 6 has different pipette than step 1 - // cy.get('[data-test="StepItem_6"]').click(batchEditClickOptions) - // // Pre-wet tip is always enabled - // cy.get('input[name="preWetTip"]').should('be.enabled') - // // well-order is always enabled - // cy.get('[id=WellOrderField_button_aspirate]').should('be.visible') - // // Aspirate Flowrate and mix disabled - // cy.get('input[name="aspirate_flowRate"]').should('be.disabled') - // cy.get('input[name="aspirate_mix_checkbox"]').should('be.disabled') - // // TipPosition Aspirate and Dispense should be disabled - // cy.get('[id=TipPositionIcon_aspirate_mmFromBottom]').should( - // 'not.be.enabled' - // ) - // cy.get('[id=TipPositionIcon_dispense_mmFromBottom]').should( - // 'not.be.enabled' - // ) - // // Dispense Flowrate and mix disabled - // cy.get('input[name="dispense_flowRate"]').should('be.disabled') - // cy.get('input[name="dispense_mix_checkbox"]').should('be.disabled') - // // Delay , Touch tip is disabled - // cy.get('input[name="aspirate_delay_checkbox"]').should('be.disabled') - // cy.get('input[name="aspirate_touchTip_checkbox"]').should('be.disabled') - // // Save button is disabled - // cy.get('button').contains('save').should('not.be.enabled') - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Verify functionality of advanced settings with same pipette and labware - // // click on step 2 in batch edit mode - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // deselecting on step 6 in batch edit mode - // cy.get('[data-test="StepItem_6"]').click(batchEditClickOptions) - // // click on step 3 , as step 2 & 3 have same pipette and labware - // cy.get('[data-test="StepItem_3"]').click(batchEditClickOptions) - // // Aspirate Flowrate and mix are enabled - // cy.get('input[name="aspirate_flowRate"]').should('be.enabled') - // cy.get('input[name="aspirate_mix_checkbox"]').should('be.enabled') - // // Dispense Flowrate and mix are enabled - // cy.get('input[name="dispense_flowRate"]').should('be.enabled') - // cy.get('input[name="dispense_mix_checkbox"]').should('be.enabled') - // // Verify invalid input in one of the fields - // cy.get('input[name="dispense_mix_checkbox"]').click({ force: true }) - // cy.get('input[name="dispense_mix_volume"]') - // .type(invalidInput) - // .should('be.empty') - // // TipPosition Aspirate and Dispense should be enabled - // cy.get('[id=TipPositionIcon_aspirate_mmFromBottom]').should( - // 'not.be.disabled' - // ) - // cy.get('[id=TipPositionIcon_dispense_mmFromBottom]').should( - // 'not.be.disabled' - // ) - // // Delay in aspirate and Dispense settings is enabled - // cy.get('input[name="aspirate_delay_checkbox"]').should('be.enabled') - // cy.get('input[name="dispense_delay_checkbox"]').should('be.enabled') - // // Touchtip in aspirate and Dispense settings is enabled - // cy.get('input[name="aspirate_touchTip_checkbox"]').should('be.enabled') - // cy.get('input[name="dispense_touchTip_checkbox"]').should('be.enabled') - // // Blowout in dispense settings is enabled - // cy.get('input[name="blowout_checkbox"]').should('be.enabled') - // cy.get('button').contains('discard changes').click() - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Verify flowrate indeterminate value - // // click on step 2 in batch edit mode - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // cy.get('input[name="aspirate_flowRate"]').click({ force: true }) - // cy.contains( - // 'The default P1000 Single-Channel GEN2 flow rate is optimal for handling aqueous liquids' - // ) - // cy.get('input[name="aspirate_flowRate_customFlowRate"]').type('100') - // cy.get('button').contains('Done').click() - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Click on step 4 as it has flowrate set to 100 from previous testcase - // cy.get('[data-test="StepItem_4"]').click(batchEditClickOptions) - // // indeterminate state in flowrate is empty - // cy.get('input[name="aspirate_flowRate"]').should('have.value', '') - // // Verify functionality of flowrate in batch edit transfer - // // Batch editing the Flowrate value - // cy.get('input[name="aspirate_flowRate"]').click({ force: true }) - // cy.contains( - // 'The default P1000 Single-Channel GEN2 flow rate is optimal for handling aqueous liquids' - // ) - // cy.get('input[name="aspirate_flowRate_customFlowRate"]').type('100') - // cy.get('button').contains('Done').click() - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Click on step 2 to verify that flowrate is updated to 100 - // cy.get('[data-test="StepItem_2"]').click() - // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click() - // // Verify that flowrate value - // cy.get('input[name="aspirate_flowRate"]').should('have.value', 100) - // // Click on step 3 to verify that flowrate is updated to 100 - // cy.get('[data-test="StepItem_3"]').click() - // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click() - // // Verify that flowrate value - // cy.get('input[name="aspirate_flowRate"]').should('have.value', 100) - // // Verify prewet tip indeterminate value - // // Click on step 2, to enter batch edit and enable prewet tip - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // enable pre-wet tip - // cy.togglePreWetTip() - // cy.get('input[name="preWetTip"]').should('be.enabled') - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Click on step 1, as it does not have prewet-tip selected - indeteminate state - // cy.get('[data-test="StepItem_1"]').click(batchEditClickOptions) - // // Check tooltip here - // cy.contains('pre-wet tip').trigger('pointerover') - // cy.get('div[role="tooltip"]').should( - // 'contain', - // 'Not all selected steps are using this setting' - // ) - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Verify mix settings indeterminate value - // // Click on step 2, to enter batch edit mode - // cy.get('[data-test="StepItem_4"]').click(batchEditClickOptions) - // // Select mix settings - // cy.mixaspirate() - // cy.get('input[name="aspirate_mix_volume"]').type('10') - // cy.get('input[name="aspirate_mix_times"]').type('2') - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Click on step 3 to generate indertminate state for mix settings. - // cy.get('[data-test="StepItem_3"]').click(batchEditClickOptions) - // // Verify the tooltip here - // cy.contains('mix').trigger('pointerover') - // cy.get('div[role="tooltip"]').should( - // 'contain', - // 'Not all selected steps are using this setting' - // ) - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Verify mix settings batch editing in transfer form - // // Click on step 2, to enter batch edit mode - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // Click on step 3 to batch edit mix settings - // cy.get('[data-test="StepItem_3"]').click(batchEditClickOptions) - // cy.get('input[name="aspirate_mix_checkbox"]').click({ force: true }) - // // Select mix settings - // cy.get('input[name="aspirate_mix_volume"]').type('10') - // cy.get('input[name="aspirate_mix_times"]').type('2') - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Click on step 2 to verify that mix has volume set to 10 with 2 repitititons - // cy.get('[data-test="StepItem_2"]').click() - // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click() - // // Verify that volume is set to 10 and repetitions to 2 - // cy.get('input[name="aspirate_mix_volume"]').should('have.value', 10) - // cy.get('input[name="aspirate_mix_times"]').should('have.value', 2) - // // Verify delay settings indeterminate value - // // Click on step 2, to enter batch edit mode - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // Select delay settings - // cy.get('input[name="aspirate_delay_checkbox"]') - // .check({ force: true }) - // .should('be.checked') - // cy.get('input[name="aspirate_delay_seconds"]').type('2') - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Click on step 3 to generate indertminate state for delay settings. - // cy.get('[data-test="StepItem_3"]').click(batchEditClickOptions) - // // Verify the tooltip here - // cy.contains('delay').trigger('pointerover') - // cy.get('div[role="tooltip"]').should( - // 'contain', - // 'Not all selected steps are using this setting' - // ) - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Verify delay settings batch editing in transfer form - // // Click on step 4, to enter batch edit mode - // cy.get('[data-test="StepItem_4"]').click(batchEditClickOptions) - // // Click on step 5 to batch edit mix settings - // cy.get('[data-test="StepItem_5"]').click(batchEditClickOptions) - // // Select delay settings - // cy.get('input[name="aspirate_delay_checkbox"]').click({ force: true }) - // cy.get('input[name="aspirate_delay_seconds"]').clear().type('2') - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Click on step 4 to verify that delay has volume set to 2 - // cy.get('[data-test="StepItem_4"]').click() - // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click() - // // Verify that volume is set to 2 and repitions to 2 - // cy.get('input[name="aspirate_delay_seconds"]').should('have.value', 2) - // // Click on step 5 to verify that delay has volume set to 2 - // cy.get('[data-test="StepItem_5"]').click() - // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click() - // // Verify that volume is set to 2 and repitions to 2 - // cy.get('input[name="aspirate_delay_seconds"]').should('have.value', 2) - // // Verify touchTip settings indeterminate value - // cy.get('[data-test="StepItem_2"]').click() - // // Click on step 2, to enter batch edit mode - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // Select touchTip settings - // cy.get('input[name="aspirate_touchTip_checkbox"]').click({ force: true }) - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Click on step 5 to generate indertminate state for touchTip settings. - // cy.get('[data-test="StepItem_5"]').click(batchEditClickOptions) - // // Verify the tooltip here - // cy.contains('touch tip').trigger('pointerover') - // cy.get('div[role="tooltip"]').should( - // 'contain', - // 'Not all selected steps are using this setting' - // ) - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // verify touchTip settings batch editing in transfer form - // // Click on step 2, to enter batch edit mode - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // Click on step 3 to batch edit mix settings - // cy.get('[data-test="StepItem_3"]').click(batchEditClickOptions) - // // Select touchTip settings - // cy.get('input[name="aspirate_touchTip_checkbox"]').click({ force: true }) - // // cy.get('[id=TipPositionModal_custom_input]').type(15) - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Click on step 2 to verify that touchTip has volume set to 2 - // cy.get('[data-test="StepItem_2"]').click() - // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click() - // // Verify that volume is set - // cy.get('[id=TipPositionField_aspirate_touchTip_mmFromBottom]').should( - // 'have.value', - // 13.78 - // ) - // // Click on step 3 to verify that touchTip has volume set - // cy.get('[data-test="StepItem_3"]').click() - // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click() - // // Verify that volume is set - // cy.get('[id=TipPositionField_aspirate_touchTip_mmFromBottom]').should( - // 'have.value', - // 13.78 - // ) - // // verify blowout settings indeterminate value - // // Click on step 2, to enter batch edit mode - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // Select blowout settings - // cy.get('input[name="blowout_checkbox"]').click({ force: true }) - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Click on step 4 to generate indertminate state for blowout settings. - // cy.get('[data-test="StepItem_4"]').click(batchEditClickOptions) - // // Verify the tooltip here - // cy.contains('blowout').trigger('pointerover') - // cy.get('div[role="tooltip"]').should( - // 'contain', - // 'Not all selected steps are using this setting' - // ) - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Verify blowout settings batch editing in transfer form - // // Click on step 2, to enter batch edit mode - // cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) - // // Click on step 3 to batch edit mix settings - // cy.get('[data-test="StepItem_3"]').click(batchEditClickOptions) - // // Select blowout settings - // cy.get('input[name="blowout_checkbox"]').click({ force: true }) - // // Click save button to save the changes - // cy.get('button').contains('save').click() - // // Exit batch edit mode - // cy.get('button').contains('exit batch edit').click() - // // Click on step 2 to verify that blowout has trash selected - // cy.get('[data-test="StepItem_2"]').click() - // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click() - // // Verify that trash is selected - // cy.get('[id=BlowoutLocationField_dropdown]').should($input => { - // const value = $input.val() - // const expectedSubstring = 'trashBin' - // expect(value).to.include(expectedSubstring) - // }) - // // Click on step 3 to verify the batch editing - // cy.get('[data-test="StepItem_3"]').click() - // cy.get('button[id="AspDispSection_settings_button_aspirate"]').click() - // // Verify that trash is selected for the blowout option - // cy.get('[id=BlowoutLocationField_dropdown]').should($input => { - // const value = $input.val() - // const expectedSubstring = 'trashBin' - // expect(value).to.include(expectedSubstring) - // }) - // }) -}) diff --git a/protocol-designer/cypress/e2e/transferSettings.cy.ts b/protocol-designer/cypress/e2e/transferSettings.cy.ts new file mode 100644 index 00000000000..a3371e0feff --- /dev/null +++ b/protocol-designer/cypress/e2e/transferSettings.cy.ts @@ -0,0 +1,77 @@ +import { Actions, Verifications, runCreateTest } from '../support/createNew' +import { UniversalActions } from '../support/universalActions' + +describe('The Redesigned Create Protocol Landing Page', () => { + beforeEach(() => { + cy.visit('/') + cy.verifyHomePage() + cy.closeAnalyticsModal() + }) + + it('content and step 1 flow works', () => { + cy.clickCreateNew() + cy.verifyCreateNewHeader() + const steps: Array = [ + Verifications.OnStep1, + Verifications.FlexSelected, + UniversalActions.Snapshot, + Actions.SelectOT2, + Verifications.OT2Selected, + UniversalActions.Snapshot, + Actions.SelectFlex, + Verifications.FlexSelected, + UniversalActions.Snapshot, + Actions.Confirm, + Verifications.OnStep2, + Actions.SingleChannelPipette50, + Verifications.StepTwo50uL, + UniversalActions.Snapshot, + Actions.Confirm, + Verifications.StepTwoPart3, + UniversalActions.Snapshot, + Actions.Confirm, + Verifications.OnStep3, + Actions.YesGripper, + Actions.Confirm, + Verifications.Step4Verification, + Actions.AddThermocycler, + Verifications.ThermocyclerImg, + Actions.AddHeaterShaker, + Verifications.HeaterShakerImg, + Actions.AddMagBlock, + Verifications.MagBlockImg, + Actions.AddTempdeck2, + Verifications.Tempdeck2Img, + Actions.Confirm, + Actions.Confirm, + Actions.Confirm, + Actions.EditProtocolA, + Actions.ChoseDeckSlotC2, + Actions.AddHardwareLabware, + Actions.ClickLabwareHeader, + Actions.ClickWellPlatesSection, + Actions.SelectArmadillo96WellPlate, + Actions.ChoseDeckSlotC2Labware, + Actions.AddLiquid, + Actions.ClickLiquidButton, + Actions.DefineLiquid, + Actions.LiquidSaveWIP, + Actions.WellSelector, + Actions.LiquidDropdown, + Verifications.LiquidPage, + UniversalActions.Snapshot, + Actions.SelectLiquidWells, + Actions.SetVolumeAndSaveforWells, + Actions.ChoseDeckSlotC3, + Actions.AddHardwareLabware, + Actions.ClickLabwareHeader, + Actions.ClickWellPlatesSection, + Actions.SelectBioRad96WellPlate, + Actions.ProtocolStepsH, + Actions.AddStep, + Verifications.TransferPopOut, + UniversalActions.Snapshot, + ] + runCreateTest(steps) + }) +}) diff --git a/protocol-designer/cypress/support/commands.ts b/protocol-designer/cypress/support/commands.ts index deaa7a4d1ff..697ddfa32f5 100644 --- a/protocol-designer/cypress/support/commands.ts +++ b/protocol-designer/cypress/support/commands.ts @@ -10,7 +10,6 @@ declare global { verifyCreateNewHeader: () => Cypress.Chainable clickCreateNew: () => Cypress.Chainable closeAnalyticsModal: () => Cypress.Chainable - closeAnnouncementModal: () => Cypress.Chainable verifyHomePage: () => Cypress.Chainable importProtocol: (protocolFile: string) => Cypress.Chainable verifyImportPageOldProtocol: () => Cypress.Chainable @@ -28,6 +27,7 @@ declare global { openDesignPage: () => Cypress.Chainable addStep: (stepName: string) => Cypress.Chainable openSettingsPage: () => Cypress.Chainable + robotSelection: (robotName: string) => Cypress.Chainable verifySettingsPage: () => Cypress.Chainable verifyCreateNewPage: () => Cypress.Chainable togglePreWetTip: () => Cypress.Chainable @@ -55,6 +55,8 @@ export const locators = { import: 'Import', createNew: 'Create new', createProtocol: 'Create a protocol', + Flex_Home: 'Opentrons Flex', + OT2_Home: 'Opentrons OT-2', importProtocol: 'Import existing protocol', settingsDataTestid: 'SettingsIconButton', settings: 'Settings', @@ -128,6 +130,17 @@ Cypress.Commands.add('importProtocol', (protocolFilePath: string) => { .selectFile(protocolFilePath, { force: true }) }) +Cypress.Commands.add('robotSelection', (robotName: string) => { + if (robotName === 'Opentrons OT-2') { + cy.contains('label', locators.OT2_Home).should('be.visible').click() + } else { + // Just checking that the selection modal works + cy.contains('label', locators.OT2_Home).should('be.visible').click() + cy.contains('label', locators.Flex_Home).should('be.visible').click() + } + cy.contains('button', 'Confirm').should('be.visible').click() +}) + // Settings Page Actions Cypress.Commands.add('openSettingsPage', () => { cy.getByTestId(locators.settingsDataTestid).click() @@ -152,16 +165,10 @@ Cypress.Commands.add('verifySettingsPage', () => { // as a reference during test migration. /// ///////////////////////////////////////////////////////////////// -Cypress.Commands.add('closeAnnouncementModal', () => { +Cypress.Commands.add('closeAnalyticsModal', () => { // ComputingSpinner sometimes covers the announcement modal button and prevents the button click // this will retry until the ComputingSpinner does not exist - cy.get('[data-test="ComputingSpinner"]', { timeout: 30000 }).should( - 'not.exist' - ) - cy.get('button') - .contains('Got It!') - .should('be.visible') - .click({ force: true }) + cy.contains('button', 'Confirm').click({ force: true }) }) // diff --git a/protocol-designer/cypress/support/createNew.ts b/protocol-designer/cypress/support/createNew.ts index b5fde778339..02430507405 100644 --- a/protocol-designer/cypress/support/createNew.ts +++ b/protocol-designer/cypress/support/createNew.ts @@ -1,33 +1,129 @@ import { executeUniversalAction, UniversalActions } from './universalActions' import { isEnumValue } from './utils' +import '../support/commands' +// ToDo Future planning should have Step 5, Step 6, and 7 verification +// Todo ProtocolOverview page. This might change from deck map revamp, +// so let's hold off until then. +// This PR unblocks Sara and I to work on this separately, so I want +// To prioritize its getting pulled into the repo +// Some day we should make a way to input variables into actions +import 'cypress-file-upload' +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + chooseDeckSlot: (slot: string) => Cypress.Chainable + } + } +} export enum Actions { SelectFlex = 'Select Opentrons Flex', SelectOT2 = 'Select Opentrons OT-2', Confirm = 'Confirm', GoBack = 'Go back', + SingleChannelPipette50 = 'Select 50uL Single-Channel Pipette', + YesGripper = 'Select Yes to gripper', + NoGripper = 'Select no to gripper', + AddThermocycler = 'Thermocycler Module GEN2', + AddHeaterShaker = 'Heater-Shaker Module GEN1', + AddTempdeck2 = 'Temperature Module GEN2', + AddMagBlock = 'Magnetic Block GEN1', + EditProtocolA = 'Blue button edit protocol', + choseDeckSlot = 'Chose each deck slot', + ChoseDeckSlotA1 = 'Choose deck slot A1', + ChoseDeckSlotA2 = 'Choose deck slot A2', + ChoseDeckSlotA3 = 'Choose deck slot A3', + ChoseDeckSlotB1 = 'Choose deck slot B1', + ChoseDeckSlotB2 = 'Choose deck slot B2', + ChoseDeckSlotB3 = 'Choose deck slot B3', + ChoseDeckSlotC1 = 'Choose deck slot C1', + ChoseDeckSlotC2 = 'Choose deck slot C2', + ChoseDeckSlotC2Labware = 'Chose labware on deck slot C2', + ChoseDeckSlotC3 = 'Choose deck slot C3', + ChoseDeckSlotD1 = 'Choose deck slot D1', + ChoseDeckSlotD2 = 'Choose deck slot D2', + ChoseDeckSlotD3 = 'Choose deck slot D3', + AddHardwareLabware = 'Adds labware to deck slot by chose deck slot', + ClickLabwareHeader = 'Click Labware', + ClickWellPlatesSection = 'Click Well plates', + SelectArmadillo96WellPlate = 'Select Armadillo 96 Well Plate', + SelectBioRad96WellPlate = 'Select Bio-Rad 96 Well Plate', + AddLiquid = 'Add liquid', + DefineLiquid = 'Define a liquid', + ClickLiquidButton = 'Click Liquid button', + LiquidSaveWIP = 'Save liquid, is functional but could use a refactor', + WellSelector = 'Select wells with strings A1, A2, etc. comman separated LIST', + LiquidDropdown = 'Dropdown for liquids when adding to well', + SelectLiquidWells = 'Select Liquid Wells', + SetVolumeAndSaveforWells = 'Set volume and save for wells', + ProtocolStepsH = 'Select Protocol Steps Header', + AddStep = 'Use after making sure you are on ProtocolStepsH or have already made a step', } export enum Verifications { OnStep1 = 'On Step 1 page.', OnStep2 = 'On Step 2 page.', + OnStep3 = 'on Step 3 page', FlexSelected = 'Opentrons Flex selected.', OT2Selected = 'Opentrons OT-2 selected.', NinetySixChannel = '96-Channel option is available.', NotNinetySixChannel = '96-Channel option is not available.', + StepTwo50uL = 'Step Two part two', + StepTwoPart3 = 'Step Two part three', + Step4Verification = 'Step 4 part 1', + ThermocyclerImg = 'Thermocycler Module GEN2', + HeaterShakerImg = 'Heater-Shaker Module GEN1', + Tempdeck2Img = 'Temperature Module GEN2', + MagBlockImg = 'Magnetic Block GEN1', + LiquidPage = 'Liquid page content is visible', + TransferPopOut = 'Verify Step 1 of the transfer function is present', } - export enum Content { Step1Title = 'Step 1', Step2Title = 'Step 2', + Step3Title = 'Step3', + Step4Title = 'Step4', AddPipette = 'Add a pipette', NinetySixChannel = '96-Channel', + SingleChannel = '1-Channel', + EightChannel = '8-Channel', + TipRack = 'Filter Tip Rack 50 µL', + PipetteType = 'Pipette type', + PipetteVolume = 'Pipette volume', + FullP50SingleName = 'Flex 1-Channel 50 μL', + FullP50TiprackName = 'Opentrons Flex 96 Filter Tip Rack 50 µL', GoBack = 'Go back', Confirm = 'Confirm', OpentronsFlex = 'Opentrons Flex', OpentronsOT2 = 'Opentrons OT-2', LetsGetStarted = 'Let’s start with the basics', WhatKindOfRobot = 'What kind of robot do you have?', + Volume50 = '50 µL', + Volume1000 = '1000 µL', + FilterTiprack50 = 'Filter Tip Rack 50 µL', + Tiprack50 = 'Tip Rack 50 µL', + Yes = 'Yes', + No = 'No', + Thermocycler = 'Thermocycler Module GEN2', + HeaterShaker = 'Heater-Shaker Module GEN1', + Tempdeck2 = 'Temperature Module GEN2', + MagBlock = 'Magnetic Block GEN1', + ModulePageH = 'Add your modules', + ModulePageB = 'Select modules to use in your protocol.', + EditProtocol = 'Edit protocol', + EditSlot = 'Edit slot', + AddLabwareToDeck = 'Add hardware/labware', + LabwareH = 'Labware', + WellPlatesCat = 'Well plates', + Armadillo96WellPlate200uL = 'Armadillo 96 Well Plate 200 µL PCR Full Skirt', + Biorad96WellPlate200uL = 'Bio-Rad 96 Well Plate 200 µL PCR', + AddLiquid = 'Add liquid', + DefineALiquid = 'Define a liquid', + LiquidButton = 'Liquid', + SampleLiquidName = 'My liquid!', + ProtocolSteps = 'Protocol steps', + AddStep = 'Add Step', } export enum Locators { @@ -38,8 +134,91 @@ export enum Locators { FlexOption = 'button:contains("Opentrons Flex")', OT2Option = 'button:contains("Opentrons OT-2")', NinetySixChannel = 'div:contains("96-Channel")', + ThermocyclerImage = 'img[alt="temperatureModuleType"]', + MagblockImage = 'img[alt="magneticBlockType"]', + HeaterShakerImage = 'img[alt="heaterShakerModuleType"]', + TemperatureModuleImage = 'img[alt="temperatureModuleType"]', + LiquidNameInput = 'input[name="name"]', + ModalShellArea = 'div[aria-label="ModalShell_ModalArea"]', + SaveButton = 'button[type="submit"]', + LiquidsDropdown = 'div[tabindex="0"].sc-bqWxrE', // Add new locator for the dropdown + LabwareSelectionLocation = '[data-testid="Toolbox_confirmButton"]', +} + +const chooseDeckSlot = ( + slot: string +): Cypress.Chainable> => { + const deckSlots: Record< + | 'A1' + | 'A2' + | 'A3' + | 'B1' + | 'B2' + | 'B3' + | 'C1' + | 'C2' + | 'C3' + | 'D1' + | 'D2' + | 'D3', + () => Cypress.Chainable> + > = { + A1: () => cy.contains('foreignObject[x="0"][y="321"]', Content.EditSlot), + A2: () => cy.contains('foreignObject[x="164"][y="321"]', Content.EditSlot), + A3: () => cy.contains('foreignObject[x="328"][y="321"]', Content.EditSlot), + B1: () => cy.contains('foreignObject[x="0"][y="214"]', Content.EditSlot), + B2: () => cy.contains('foreignObject[x="164"][y="214"]', Content.EditSlot), + B3: () => cy.contains('foreignObject[x="328"][y="214"]', Content.EditSlot), + C1: () => cy.contains('foreignObject[x="0"][y="107"]', Content.EditSlot), + C2: () => cy.contains('foreignObject[x="164"][y="107"]', Content.EditSlot), + C3: () => cy.contains('foreignObject[x="328"][y="107"]', Content.EditSlot), + D1: () => cy.contains('foreignObject[x="0"][y="0"]', Content.EditSlot), + D2: () => cy.contains('foreignObject[x="164"][y="0"]', Content.EditSlot), + D3: () => cy.contains('foreignObject[x="328"][y="0"]', Content.EditSlot), + } + + const slotAction = deckSlots[slot as keyof typeof deckSlots] + + if (typeof slotAction === 'function') { + return slotAction() // Return the chainable object + } else { + throw new Error(`Slot ${slot} not found in deck slots.`) + } +} +// Well name selection for liquids and in general +const selectWells = (wells: string[]): void => { + // Define a dictionary of well selectors + const wellSelectors: Record< + string, + () => Cypress.Chainable> + > = {} + + // Populate the dictionary dynamically + const rows = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] + const columns = Array.from({ length: 12 }, (_, i) => (i + 1).toString()) + + rows.forEach(row => { + columns.forEach(column => { + const wellName = `${row}${column}` + wellSelectors[wellName] = () => + cy.get(`circle[data-wellname="${wellName}"]`).click({ force: true }) + }) + }) + + // Iterate over the wells array and click the corresponding wells + wells.forEach(well => { + const wellAction = wellSelectors[well] + if (typeof wellAction === 'function') { + wellAction() // Click the well + } else { + throw new Error(`Well ${well} not found.`) + } + }) } +// Example usage +// selectWells(['A1', 'B3', 'H12']) + const executeAction = (action: Actions | UniversalActions): void => { if (isEnumValue([UniversalActions], [action])) { executeUniversalAction(action as UniversalActions) @@ -50,6 +229,7 @@ const executeAction = (action: Actions | UniversalActions): void => { case Actions.SelectFlex: cy.contains(Content.OpentronsFlex).should('be.visible').click() break + case Actions.SelectOT2: cy.contains(Content.OpentronsOT2).should('be.visible').click() break @@ -59,6 +239,147 @@ const executeAction = (action: Actions | UniversalActions): void => { case Actions.GoBack: cy.contains(Content.GoBack).should('be.visible').click() break + case Actions.SingleChannelPipette50: + cy.contains('label', Content.SingleChannel) + .should('exist') + .and('be.visible') + .click() + cy.contains(Content.Volume50).click() + cy.contains(Content.Tiprack50).click() + // ToDo after PR, why does this click Tiprack50 again + // instead of clicking the filter tiprack? + // cy.contains(Content.FilterTiprack50).click() + break + case Actions.AddThermocycler: + cy.contains(Content.Thermocycler).click() + break + case Actions.AddHeaterShaker: + cy.contains(Content.HeaterShaker).click() + break + case Actions.AddTempdeck2: + cy.contains(Content.Tempdeck2).click() + break + case Actions.AddMagBlock: + cy.contains(Content.MagBlock).click() + break + case Actions.YesGripper: + cy.contains(Content.Yes).click() + break + case Actions.NoGripper: + cy.contains(Content.No).click() + break + case Actions.EditProtocolA: + cy.contains(Content.EditProtocol).click() + break + case Actions.ChoseDeckSlotA1: + chooseDeckSlot('A1').click() + break + case Actions.ChoseDeckSlotA2: + chooseDeckSlot('A2').click() + break + case Actions.ChoseDeckSlotA3: + chooseDeckSlot('A3').click() + break + case Actions.ChoseDeckSlotB1: + chooseDeckSlot('B1').click() + break + case Actions.ChoseDeckSlotB2: + chooseDeckSlot('B2').click() + break + case Actions.ChoseDeckSlotB3: + chooseDeckSlot('B3').click() + break + case Actions.ChoseDeckSlotC1: + chooseDeckSlot('C1').click() + break + case Actions.ChoseDeckSlotC2: + chooseDeckSlot('C2').click() + break + case Actions.ChoseDeckSlotC3: + chooseDeckSlot('C3').click() + break + case Actions.ChoseDeckSlotD1: + chooseDeckSlot('D1').click() + break + case Actions.ChoseDeckSlotD2: + chooseDeckSlot('D2').click() + break + case Actions.ChoseDeckSlotD3: + chooseDeckSlot('D3').click() + break + case Actions.AddHardwareLabware: // New case + cy.contains(Content.AddLabwareToDeck).click() + break + case Actions.ClickLabwareHeader: // New case + cy.contains(Content.LabwareH).click() + break + case Actions.ClickWellPlatesSection: // New case + cy.contains(Content.WellPlatesCat).click() + break + case Actions.ChoseDeckSlotC2Labware: + // Todo Investigate making a dictionary of slot editing. + // Maybe next PR + chooseDeckSlot('C2') + .find('.Box-sc-8ozbhb-0.kIDovv') + .find('a[role="button"]') + .contains(Content.EditSlot) + .click({ force: true }) + break + case Actions.SelectArmadillo96WellPlate: // New case for selecting Armadillo plate + cy.contains(Content.Armadillo96WellPlate200uL).click({ force: true }) + cy.get(Locators.LabwareSelectionLocation).click({ force: true }) + break + case Actions.SelectBioRad96WellPlate: // New case for selecting Armadillo plate + cy.contains(Content.Biorad96WellPlate200uL).click({ force: true }) + cy.get(Locators.LabwareSelectionLocation).click({ force: true }) + break + + case Actions.AddLiquid: // New case for "Add liquid" + cy.contains('button', Content.AddLiquid).click() + break + case Actions.ClickLiquidButton: // New case for "Liquid button" + cy.contains('button', Content.LiquidButton).click() + break + case Actions.DefineLiquid: // New case for "Define a liquid" + cy.contains('button', Content.DefineALiquid).click() + break + case Actions.LiquidSaveWIP: + cy.get(Locators.LiquidNameInput) // Locate the input with name="name" + .type(Content.SampleLiquidName) + + cy.get(Locators.ModalShellArea) + .find('form') // Target the form inside the modal + .invoke('submit', (e: SubmitEvent) => { + e.preventDefault() // Prevent default form submission + }) + + cy.get(Locators.ModalShellArea) + .find(Locators.SaveButton) // Locate the Save button + .contains('Save') + .click({ force: true }) // Trigger the Save button + break + case Actions.WellSelector: + selectWells(['A1', 'A2']) + break + case Actions.LiquidDropdown: // New case for dropdown + cy.get(Locators.LiquidsDropdown) + .should('be.visible') // Ensure the dropdown is visible + .click() // Click the dropdown + break + case Actions.SelectLiquidWells: + cy.contains('My liquid!').click() // Action for clicking 'My liquid!' + break + case Actions.SetVolumeAndSaveforWells: + cy.get('input[name="volume"]').type(`150`) // Set volume + cy.contains('button', 'Save').click() // Click Save button + cy.contains('button', 'Done').click({ force: true }) // Click Done button, forcing click if necessary + break + case Actions.ProtocolStepsH: + cy.contains('button', Content.ProtocolSteps).click() + break + case Actions.AddStep: + cy.contains('button', Content.AddStep).click() + break default: throw new Error(`Unrecognized action: ${action as string}`) } @@ -93,6 +414,65 @@ const verifyStep = (verification: Verifications): void => { case Verifications.NotNinetySixChannel: cy.contains(Content.NinetySixChannel).should('not.exist') break + case Verifications.StepTwo50uL: + // This function should get used after you select 50uL fully + cy.contains(Content.PipetteVolume) + cy.contains(Content.Volume50).should('be.visible') + cy.contains(Content.Volume1000).should('be.visible') + cy.contains(Content.Tiprack50).should('be.visible') + cy.contains(Content.FilterTiprack50).should('be.visible') + break + case Verifications.StepTwoPart3: + // This function should get used after you select 50uL fully + cy.contains(Content.FullP50SingleName).should('be.visible') + cy.contains(Content.FullP50TiprackName).should('be.visible') + cy.contains('Left Mount').should('be.visible') + cy.contains(Content.Step2Title) + cy.contains('Robot pipettes') + cy.contains(Content.AddPipette) + break + case Verifications.OnStep3: + cy.contains('Add a gripper').should('be.visible') + cy.contains( + 'Do you want to move labware automatically with the gripper?' + ).should('be.visible') + cy.contains(Content.Yes).should('be.visible') + cy.contains(Content.No).should('be.visible') + break + case Verifications.Step4Verification: + cy.contains(Content.ModulePageH).should('be.visible') + cy.contains(Content.ModulePageB).should('be.visible') + cy.contains(Content.Thermocycler).should('be.visible') + cy.contains(Content.HeaterShaker).should('be.visible') + cy.contains(Content.MagBlock).should('be.visible') + cy.contains(Content.Tempdeck2).should('be.visible') + break + case Verifications.ThermocyclerImg: + cy.get(Locators.TemperatureModuleImage).should('be.visible') + break + case Verifications.HeaterShakerImg: + cy.get(Locators.HeaterShakerImage).should('be.visible') + break + case Verifications.Tempdeck2Img: + cy.contains(Content.Tempdeck2).should('be.visible') + break + case Verifications.LiquidPage: + cy.contains('Liquid').should('be.visible') + cy.contains('Add liquid').should('be.visible') + cy.contains('Liquid volume by well').should('be.visible') + cy.contains('Cancel').should('be.visible') + break + case Verifications.TransferPopOut: + cy.contains('button', 'Transfer').should('be.visible').click() + cy.contains('Source labware') + cy.contains('Select source wells') + cy.contains('Destination labware') + cy.contains('Volume per well') + cy.contains('Tip handling') + cy.contains('Tip handling') + cy.contains('Tip drop location') + break + default: throw new Error( `Unrecognized verification: ${verification as Verifications}` From d3b1b9c1d7f6c1b539c2ed43db5e049cf07651e8 Mon Sep 17 00:00:00 2001 From: Jethary Alcid <66035149+jerader@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:26:15 -0500 Subject: [PATCH 09/10] feat(protocol-designer): yaml upload and download artifact version to 4 (#17229) github actions upload-artifact and download-artifact v3 was deprecated so this updates to v4. --- .github/workflows/pd-test-build-deploy.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pd-test-build-deploy.yaml b/.github/workflows/pd-test-build-deploy.yaml index ce28d79e9a5..babebb5a918 100644 --- a/.github/workflows/pd-test-build-deploy.yaml +++ b/.github/workflows/pd-test-build-deploy.yaml @@ -164,7 +164,7 @@ jobs: run: | make -C protocol-designer NODE_ENV=development - name: 'upload github artifact' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: 'pd-artifact' path: protocol-designer/dist @@ -197,7 +197,7 @@ jobs: const { buildComplexEnvVars } = require(`${process.env.GITHUB_WORKSPACE}/.github/workflows/utils.js`) buildComplexEnvVars(core, context) - name: 'download PD build' - uses: 'actions/download-artifact@v3' + uses: 'actions/download-artifact@v4' with: name: pd-artifact path: ./dist From a3f541d8162fa5f3381e2806aabac68af2399432 Mon Sep 17 00:00:00 2001 From: Brayan Almonte Date: Thu, 9 Jan 2025 14:22:05 -0500 Subject: [PATCH 10/10] feat(api, robot-server, shared-data): add FlexStacker module to the hardware controller and robot server. (#17187) --- .../drivers/flex_stacker/__init__.py | 9 +- .../drivers/flex_stacker/abstract.py | 40 +- .../opentrons/drivers/flex_stacker/driver.py | 136 ++++- .../drivers/flex_stacker/simulator.py | 59 +- .../opentrons/drivers/flex_stacker/types.py | 44 +- .../hardware_control/modules/__init__.py | 8 + .../hardware_control/modules/flex_stacker.py | 379 +++++++++++++ .../hardware_control/modules/types.py | 85 +++ .../hardware_control/modules/utils.py | 3 + api/src/opentrons/protocol_engine/types.py | 9 + .../drivers/flex_stacker/test_driver.py | 47 ++ .../modules/test_hc_flexstacker.py | 52 ++ .../hardware_control/test_modules.py | 54 +- .../modules/module_data_mapper.py | 20 + .../robot_server/modules/module_models.py | 41 +- .../definitions/3/flexStackerModuleV1.json | 524 ++++++++++++++++++ .../module/definitions/3/flexStackerV1.json | 184 ------ shared-data/module/schemas/3.json | 4 +- .../opentrons_shared_data/module/types.py | 8 +- 19 files changed, 1473 insertions(+), 233 deletions(-) create mode 100644 api/src/opentrons/hardware_control/modules/flex_stacker.py create mode 100644 api/tests/opentrons/hardware_control/modules/test_hc_flexstacker.py create mode 100644 shared-data/module/definitions/3/flexStackerModuleV1.json delete mode 100644 shared-data/module/definitions/3/flexStackerV1.json diff --git a/api/src/opentrons/drivers/flex_stacker/__init__.py b/api/src/opentrons/drivers/flex_stacker/__init__.py index cd4866c179a..66b4cda546b 100644 --- a/api/src/opentrons/drivers/flex_stacker/__init__.py +++ b/api/src/opentrons/drivers/flex_stacker/__init__.py @@ -1,9 +1,12 @@ -from .abstract import AbstractStackerDriver -from .driver import FlexStackerDriver +from .abstract import AbstractFlexStackerDriver +from .driver import FlexStackerDriver, STACKER_MOTION_CONFIG from .simulator import SimulatingDriver +from . import types as FlexStackerTypes __all__ = [ - "AbstractStackerDriver", + "AbstractFlexStackerDriver", "FlexStackerDriver", "SimulatingDriver", + "FlexStackerTypes", + "STACKER_MOTION_CONFIG", ] diff --git a/api/src/opentrons/drivers/flex_stacker/abstract.py b/api/src/opentrons/drivers/flex_stacker/abstract.py index 5ba3cdcb026..222e6715086 100644 --- a/api/src/opentrons/drivers/flex_stacker/abstract.py +++ b/api/src/opentrons/drivers/flex_stacker/abstract.py @@ -1,6 +1,8 @@ -from typing import Protocol +from typing import List, Protocol from .types import ( + LimitSwitchStatus, + MoveResult, StackerAxis, PlatformStatus, Direction, @@ -10,7 +12,7 @@ ) -class AbstractStackerDriver(Protocol): +class AbstractFlexStackerDriver(Protocol): """Protocol for the Stacker driver.""" async def connect(self) -> None: @@ -25,10 +27,6 @@ async def is_connected(self) -> bool: """Check connection to stacker.""" ... - async def update_firmware(self, firmware_file_path: str) -> None: - """Updates the firmware on the device.""" - ... - async def get_device_info(self) -> StackerInfo: """Get Device Info.""" ... @@ -37,10 +35,26 @@ async def set_serial_number(self, sn: str) -> bool: """Set Serial Number.""" ... + async def enable_motors(self, axis: List[StackerAxis]) -> bool: + """Enables the axis motor if present, disables it otherwise.""" + ... + async def stop_motors(self) -> bool: """Stop all motor movement.""" ... + async def set_run_current(self, axis: StackerAxis, current: float) -> bool: + """Set axis peak run current in amps.""" + ... + + async def set_ihold_current(self, axis: StackerAxis, current: float) -> bool: + """Set axis hold current in amps.""" + ... + + async def get_motion_params(self, axis: StackerAxis) -> MoveParams: + """Get the motion parameters used by the given axis motor.""" + ... + async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: """Get limit switch status. @@ -48,6 +62,10 @@ async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> boo """ ... + async def get_limit_switches_status(self) -> LimitSwitchStatus: + """Get limit switch statuses for all axes.""" + ... + async def get_platform_sensor(self, direction: Direction) -> bool: """Get platform sensor status. @@ -68,13 +86,13 @@ async def get_hopper_door_closed(self) -> bool: async def move_in_mm( self, axis: StackerAxis, distance: float, params: MoveParams | None = None - ) -> bool: - """Move axis.""" + ) -> MoveResult: + """Move axis by the given distance in mm.""" ... async def move_to_limit_switch( self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None - ) -> bool: + ) -> MoveResult: """Move until limit switch is triggered.""" ... @@ -87,3 +105,7 @@ async def set_led( ) -> bool: """Set LED color of status bar.""" ... + + async def enter_programming_mode(self) -> None: + """Reboot into programming mode""" + ... diff --git a/api/src/opentrons/drivers/flex_stacker/driver.py b/api/src/opentrons/drivers/flex_stacker/driver.py index 83671023772..366ea08b5f5 100644 --- a/api/src/opentrons/drivers/flex_stacker/driver.py +++ b/api/src/opentrons/drivers/flex_stacker/driver.py @@ -1,13 +1,14 @@ import asyncio import re -from typing import Optional +from typing import List, Optional from opentrons.drivers.command_builder import CommandBuilder from opentrons.drivers.asyncio.communication import AsyncResponseSerialConnection -from .abstract import AbstractStackerDriver +from .abstract import AbstractFlexStackerDriver from .types import ( GCODE, + MoveResult, StackerAxis, PlatformStatus, Direction, @@ -28,7 +29,59 @@ GCODE_ROUNDING_PRECISION = 2 -class FlexStackerDriver(AbstractStackerDriver): +STACKER_MOTION_CONFIG = { + StackerAxis.X: { + "home": MoveParams( + StackerAxis.X, + max_speed=10.0, + acceleration=100.0, + max_speed_discont=40, + current=1.5, + ), + "move": MoveParams( + StackerAxis.X, + max_speed=200.0, + acceleration=1500.0, + max_speed_discont=40, + current=1.0, + ), + }, + StackerAxis.Z: { + "home": MoveParams( + StackerAxis.Z, + max_speed=10.0, + acceleration=100.0, + max_speed_discont=40, + current=1.5, + ), + "move": MoveParams( + StackerAxis.Z, + max_speed=200.0, + acceleration=500.0, + max_speed_discont=40, + current=1.5, + ), + }, + StackerAxis.L: { + "home": MoveParams( + StackerAxis.L, + max_speed=100.0, + acceleration=800.0, + max_speed_discont=40, + current=0.8, + ), + "move": MoveParams( + StackerAxis.L, + max_speed=100.0, + acceleration=800.0, + max_speed_discont=40, + current=0.6, + ), + }, +} + + +class FlexStackerDriver(AbstractFlexStackerDriver): """FLEX Stacker driver.""" @classmethod @@ -76,6 +129,27 @@ def parse_door_closed(cls, response: str) -> bool: raise ValueError(f"Incorrect Response for door closed: {response}") return bool(int(match.group(1))) + @classmethod + def parse_move_params(cls, response: str) -> MoveParams: + """Parse move params.""" + field_names = MoveParams.get_fields() + pattern = r"\s".join( + [ + rf"{f}:(?P<{f}>(\d*\.)?\d+)" if f != "M" else rf"{f}:(?P<{f}>[X,Z,L])" + for f in field_names + ] + ) + _RE = re.compile(f"^{GCODE.GET_MOVE_PARAMS} {pattern}$") + m = _RE.match(response) + if not m: + raise ValueError(f"Incorrect Response for move params: {response}") + return MoveParams( + axis=StackerAxis(m.group("M")), + max_speed=float(m.group("V")), + acceleration=float(m.group("A")), + max_speed_discont=float(m.group("D")), + ) + @classmethod def append_move_params( cls, command: CommandBuilder, params: MoveParams | None @@ -148,6 +222,16 @@ async def set_serial_number(self, sn: str) -> bool: raise ValueError(f"Incorrect Response for set serial number: {resp}") return True + async def enable_motors(self, axis: List[StackerAxis]) -> bool: + """Enables the axis motor if present, disables it otherwise.""" + command = GCODE.ENABLE_MOTORS.build_command() + for a in axis: + command.add_element(a.name) + resp = await self._connection.send_command(command) + if not re.match(rf"^{GCODE.ENABLE_MOTORS}$", resp): + raise ValueError(f"Incorrect Response for enable motors: {resp}") + return True + async def stop_motors(self) -> bool: """Stop all motor movement.""" resp = await self._connection.send_command(GCODE.STOP_MOTORS.build_command()) @@ -155,6 +239,31 @@ async def stop_motors(self) -> bool: raise ValueError(f"Incorrect Response for stop motors: {resp}") return True + async def set_run_current(self, axis: StackerAxis, current: float) -> bool: + """Set axis peak run current in amps.""" + resp = await self._connection.send_command( + GCODE.SET_RUN_CURRENT.build_command().add_float(axis.name, current) + ) + if not re.match(rf"^{GCODE.SET_RUN_CURRENT}$", resp): + raise ValueError(f"Incorrect Response for set run current: {resp}") + return True + + async def set_ihold_current(self, axis: StackerAxis, current: float) -> bool: + """Set axis hold current in amps.""" + resp = await self._connection.send_command( + GCODE.SET_IHOLD_CURRENT.build_command().add_float(axis.name, current) + ) + if not re.match(rf"^{GCODE.SET_IHOLD_CURRENT}$", resp): + raise ValueError(f"Incorrect Response for set ihold current: {resp}") + return True + + async def get_motion_params(self, axis: StackerAxis) -> MoveParams: + """Get the motion parameters used by the given axis motor.""" + response = await self._connection.send_command( + GCODE.GET_MOVE_PARAMS.build_command().add_element(axis.name) + ) + return self.parse_move_params(response) + async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: """Get limit switch status. @@ -197,8 +306,8 @@ async def get_hopper_door_closed(self) -> bool: async def move_in_mm( self, axis: StackerAxis, distance: float, params: MoveParams | None = None - ) -> bool: - """Move axis.""" + ) -> MoveResult: + """Move axis by the given distance in mm.""" command = self.append_move_params( GCODE.MOVE_TO.build_command().add_float( axis.name, distance, GCODE_ROUNDING_PRECISION @@ -208,11 +317,12 @@ async def move_in_mm( resp = await self._connection.send_command(command) if not re.match(rf"^{GCODE.MOVE_TO}$", resp): raise ValueError(f"Incorrect Response for move to: {resp}") - return True + # TODO: handle STALL_ERROR + return MoveResult.NO_ERROR async def move_to_limit_switch( self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None - ) -> bool: + ) -> MoveResult: """Move until limit switch is triggered.""" command = self.append_move_params( GCODE.MOVE_TO_SWITCH.build_command().add_int(axis.name, direction.value), @@ -221,7 +331,8 @@ async def move_to_limit_switch( resp = await self._connection.send_command(command) if not re.match(rf"^{GCODE.MOVE_TO_SWITCH}$", resp): raise ValueError(f"Incorrect Response for move to switch: {resp}") - return True + # TODO: handle STALL_ERROR + return MoveResult.NO_ERROR async def home_axis(self, axis: StackerAxis, direction: Direction) -> bool: """Home axis.""" @@ -254,7 +365,8 @@ async def set_led( raise ValueError(f"Incorrect Response for set led: {resp}") return True - async def update_firmware(self, firmware_file_path: str) -> None: - """Updates the firmware on the device.""" - # TODO: Implement firmware update - pass + async def enter_programming_mode(self) -> None: + """Reboot into programming mode""" + command = GCODE.ENTER_BOOTLOADER.build_command() + await self._connection.send_dfu_command(command) + await self._connection.close() diff --git a/api/src/opentrons/drivers/flex_stacker/simulator.py b/api/src/opentrons/drivers/flex_stacker/simulator.py index 1e0b59b19de..1ceedabf146 100644 --- a/api/src/opentrons/drivers/flex_stacker/simulator.py +++ b/api/src/opentrons/drivers/flex_stacker/simulator.py @@ -1,9 +1,11 @@ -from typing import Optional +from typing import List, Optional from opentrons.util.async_helpers import ensure_yield -from .abstract import AbstractStackerDriver +from .abstract import AbstractFlexStackerDriver from .types import ( + LEDColor, + MoveResult, StackerAxis, PlatformStatus, Direction, @@ -14,7 +16,7 @@ ) -class SimulatingDriver(AbstractStackerDriver): +class SimulatingDriver(AbstractFlexStackerDriver): """FLEX Stacker driver simulator.""" def __init__(self, serial_number: Optional[str] = None) -> None: @@ -60,11 +62,27 @@ async def set_serial_number(self, sn: str) -> bool: """Set Serial Number.""" return True + async def enable_motors(self, axis: List[StackerAxis]) -> bool: + """Enables the axis motor if present, disables it otherwise.""" + return True + @ensure_yield - async def stop_motor(self) -> bool: - """Stop motor movement.""" + async def stop_motors(self) -> bool: + """Stop all motor movement.""" + return True + + async def set_run_current(self, axis: StackerAxis, current: float) -> bool: + """Set axis peak run current in amps.""" + return True + + async def set_ihold_current(self, axis: StackerAxis, current: float) -> bool: + """Set axis hold current in amps.""" return True + async def get_motion_params(self, axis: StackerAxis) -> MoveParams: + """Get the motion parameters used by the given axis motor.""" + return MoveParams(axis, 1, 1, 1) + @ensure_yield async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool: """Get limit switch status. @@ -78,12 +96,15 @@ async def get_limit_switches_status(self) -> LimitSwitchStatus: """Get limit switch statuses for all axes.""" return self._limit_switch_status - @ensure_yield - async def get_platform_sensor_status(self) -> PlatformStatus: + async def get_platform_sensor(self, direction: Direction) -> bool: """Get platform sensor status. - :return: True if platform is detected, False otherwise + :return: True if platform is present, False otherwise """ + return True + + async def get_platform_status(self) -> PlatformStatus: + """Get platform status.""" return self._platform_sensor_status @ensure_yield @@ -97,13 +118,27 @@ async def get_hopper_door_closed(self) -> bool: @ensure_yield async def move_in_mm( self, axis: StackerAxis, distance: float, params: MoveParams | None = None - ) -> bool: - """Move axis.""" - return True + ) -> MoveResult: + """Move axis by the given distance in mm.""" + return MoveResult.NO_ERROR @ensure_yield async def move_to_limit_switch( self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None - ) -> bool: + ) -> MoveResult: """Move until limit switch is triggered.""" + return MoveResult.NO_ERROR + + async def home_axis(self, axis: StackerAxis, direction: Direction) -> bool: + """Home axis.""" return True + + async def set_led( + self, power: float, color: LEDColor | None = None, external: bool | None = None + ) -> bool: + """Set LED color.""" + return True + + async def enter_programming_mode(self) -> None: + """Reboot into programming mode""" + pass diff --git a/api/src/opentrons/drivers/flex_stacker/types.py b/api/src/opentrons/drivers/flex_stacker/types.py index 4035aaaa755..9f8e8825b93 100644 --- a/api/src/opentrons/drivers/flex_stacker/types.py +++ b/api/src/opentrons/drivers/flex_stacker/types.py @@ -1,6 +1,6 @@ from enum import Enum from dataclasses import dataclass, fields -from typing import List +from typing import List, Dict, Optional from opentrons.drivers.command_builder import CommandBuilder @@ -11,13 +11,17 @@ class GCODE(str, Enum): MOVE_TO_SWITCH = "G5" HOME_AXIS = "G28" STOP_MOTORS = "M0" + ENABLE_MOTORS = "M17" GET_RESET_REASON = "M114" DEVICE_INFO = "M115" GET_LIMIT_SWITCH = "M119" - SET_LED = "M200" + GET_MOVE_PARAMS = "M120" GET_PLATFORM_SENSOR = "M121" GET_DOOR_SWITCH = "M122" + SET_LED = "M200" SET_SERIAL_NUMBER = "M996" + SET_RUN_CURRENT = "M906" + SET_IHOLD_CURRENT = "M907" ENTER_BOOTLOADER = "dfu" def build_command(self) -> CommandBuilder: @@ -45,6 +49,14 @@ class StackerInfo: hw: HardwareRevision sn: str + def to_dict(self) -> Dict[str, str]: + """Build command.""" + return { + "serial": self.sn, + "version": self.fw, + "model": self.hw.value, + } + class StackerAxis(Enum): """Stacker Axis.""" @@ -128,11 +140,33 @@ def get(self, direction: Direction) -> bool: """Get platform status.""" return self.E if direction == Direction.EXTENT else self.R + def to_dict(self) -> Dict[str, bool]: + """Dict of the data.""" + return { + "extent": self.E, + "retract": self.R, + } + @dataclass class MoveParams: """Move Parameters.""" - max_speed: float | None = None - acceleration: float | None = None - max_speed_discont: float | None = None + axis: Optional[StackerAxis] = None + max_speed: Optional[float] = None + acceleration: Optional[float] = None + max_speed_discont: Optional[float] = None + current: Optional[float] = 0 + + @classmethod + def get_fields(cls) -> List[str]: + """Get parsing fields.""" + return ["M", "V", "A", "D"] + + +class MoveResult(str, Enum): + """The result of a move command.""" + + NO_ERROR = "ok" + STALL_ERROR = "stall" + UNKNOWN_ERROR = "unknown" diff --git a/api/src/opentrons/hardware_control/modules/__init__.py b/api/src/opentrons/hardware_control/modules/__init__.py index 67a6c442f39..35ae63eacbe 100644 --- a/api/src/opentrons/hardware_control/modules/__init__.py +++ b/api/src/opentrons/hardware_control/modules/__init__.py @@ -4,6 +4,7 @@ from .thermocycler import Thermocycler from .heater_shaker import HeaterShaker from .absorbance_reader import AbsorbanceReader +from .flex_stacker import FlexStacker from .update import update_firmware from .utils import MODULE_TYPE_BY_NAME, build from .types import ( @@ -19,6 +20,9 @@ MagneticStatus, HeaterShakerStatus, AbsorbanceReaderStatus, + PlatformState, + StackerAxisState, + FlexStackerStatus, SpeedStatus, LiveData, ) @@ -55,4 +59,8 @@ "AbsorbanceReaderStatus", "AbsorbanceReaderDisconnectedError", "ModuleDisconnectedCallback", + "FlexStacker", + "FlexStackerStatus", + "PlatformState", + "StackerAxisState", ] diff --git a/api/src/opentrons/hardware_control/modules/flex_stacker.py b/api/src/opentrons/hardware_control/modules/flex_stacker.py new file mode 100644 index 00000000000..5ded3b391b3 --- /dev/null +++ b/api/src/opentrons/hardware_control/modules/flex_stacker.py @@ -0,0 +1,379 @@ +from __future__ import annotations + +import asyncio +import logging +from typing import Dict, Optional, Mapping + +from opentrons.drivers.flex_stacker.types import ( + Direction, + MoveParams, + MoveResult, + StackerAxis, +) +from opentrons.drivers.rpi_drivers.types import USBPort +from opentrons.drivers.flex_stacker.driver import ( + STACKER_MOTION_CONFIG, + FlexStackerDriver, +) +from opentrons.drivers.flex_stacker.abstract import AbstractFlexStackerDriver +from opentrons.drivers.flex_stacker.simulator import SimulatingDriver +from opentrons.hardware_control.execution_manager import ExecutionManager +from opentrons.hardware_control.poller import Reader, Poller +from opentrons.hardware_control.modules import mod_abc, update +from opentrons.hardware_control.modules.types import ( + FlexStackerStatus, + HopperDoorState, + LatchState, + ModuleDisconnectedCallback, + ModuleType, + PlatformState, + StackerAxisState, + UploadFunction, + LiveData, +) + +log = logging.getLogger(__name__) + +POLL_PERIOD = 1.0 +SIMULATING_POLL_PERIOD = POLL_PERIOD / 20.0 + +DFU_PID = "df11" + +# Distance in mm the latch can travel to open/close +LATCH_TRAVEL = 25.0 + + +class FlexStacker(mod_abc.AbstractModule): + """Hardware control interface for an attached Flex-Stacker module.""" + + MODULE_TYPE = ModuleType.FLEX_STACKER + + @classmethod + async def build( + cls, + port: str, + usb_port: USBPort, + execution_manager: ExecutionManager, + hw_control_loop: asyncio.AbstractEventLoop, + poll_interval_seconds: Optional[float] = None, + simulating: bool = False, + sim_model: Optional[str] = None, + sim_serial_number: Optional[str] = None, + disconnected_callback: ModuleDisconnectedCallback = None, + ) -> "FlexStacker": + """ + Build a FlexStacker + + Args: + port: The port to connect to + usb_port: USB Port + execution_manager: Execution manager. + hw_control_loop: The event loop running in the hardware control thread. + poll_interval_seconds: Poll interval override. + simulating: whether to build a simulating driver + loop: Loop + sim_model: The model name used by simulator + disconnected_callback: Callback to inform the module controller that the device was disconnected + + Returns: + FlexStacker instance + """ + driver: AbstractFlexStackerDriver + if not simulating: + driver = await FlexStackerDriver.create(port=port, loop=hw_control_loop) + poll_interval_seconds = poll_interval_seconds or POLL_PERIOD + else: + driver = SimulatingDriver(serial_number=sim_serial_number) + poll_interval_seconds = poll_interval_seconds or SIMULATING_POLL_PERIOD + + reader = FlexStackerReader(driver=driver) + poller = Poller(reader=reader, interval=poll_interval_seconds) + module = cls( + port=port, + usb_port=usb_port, + driver=driver, + reader=reader, + poller=poller, + device_info=(await driver.get_device_info()).to_dict(), + hw_control_loop=hw_control_loop, + execution_manager=execution_manager, + disconnected_callback=disconnected_callback, + ) + + try: + await poller.start() + except Exception: + log.exception(f"First read of Flex-Stacker on port {port} failed") + + return module + + def __init__( + self, + port: str, + usb_port: USBPort, + execution_manager: ExecutionManager, + driver: AbstractFlexStackerDriver, + reader: FlexStackerReader, + poller: Poller, + device_info: Mapping[str, str], + hw_control_loop: asyncio.AbstractEventLoop, + disconnected_callback: ModuleDisconnectedCallback = None, + ): + super().__init__( + port=port, + usb_port=usb_port, + hw_control_loop=hw_control_loop, + execution_manager=execution_manager, + disconnected_callback=disconnected_callback, + ) + self._device_info = device_info + self._driver = driver + self._reader = reader + self._poller = poller + self._stacker_status = FlexStackerStatus.IDLE + + async def cleanup(self) -> None: + """Stop the poller task""" + await self._poller.stop() + await self._driver.disconnect() + + @classmethod + def name(cls) -> str: + """Used for picking up serial port symlinks""" + return "flexstacker" + + def firmware_prefix(self) -> str: + """The prefix used for looking up firmware""" + return "flex-stacker" + + @staticmethod + def _model_from_revision(revision: Optional[str]) -> str: + """Defines the revision -> model mapping""" + return "flexStackerModuleV1" + + def model(self) -> str: + return self._model_from_revision(self._device_info.get("model")) + + @property + def latch_state(self) -> LatchState: + """The state of the latch.""" + return LatchState.from_state(self.limit_switch_status[StackerAxis.L]) + + @property + def platform_state(self) -> PlatformState: + """The state of the platform.""" + return self._reader.platform_state + + @property + def hopper_door_state(self) -> HopperDoorState: + """The status of the hopper door.""" + return HopperDoorState.from_state(self._reader.hopper_door_closed) + + @property + def limit_switch_status(self) -> Dict[StackerAxis, StackerAxisState]: + """The status of the Limit switches.""" + return self._reader.limit_switch_status + + @property + def device_info(self) -> Mapping[str, str]: + return self._device_info + + @property + def status(self) -> FlexStackerStatus: + """Module status or error state details.""" + return self._stacker_status + + @property + def is_simulated(self) -> bool: + return isinstance(self._driver, SimulatingDriver) + + @property + def live_data(self) -> LiveData: + return { + "status": self.status.value, + "data": { + "latchState": self.latch_state.value, + "platformState": self.platform_state.value, + "hopperDoorState": self.hopper_door_state.value, + "axisStateX": self.limit_switch_status[StackerAxis.X].value, + "axisStateZ": self.limit_switch_status[StackerAxis.Z].value, + "errorDetails": self._reader.error, + }, + } + + async def prep_for_update(self) -> str: + await self._poller.stop() + await self._driver.stop_motors() + await self._driver.enter_programming_mode() + # flex stacker has three unique "devices" over DFU + dfu_info = await update.find_dfu_device(pid=DFU_PID, expected_device_count=3) + return dfu_info + + def bootloader(self) -> UploadFunction: + return update.upload_via_dfu + + async def deactivate(self, must_be_running: bool = True) -> None: + await self._driver.stop_motors() + + async def move_axis( + self, + axis: StackerAxis, + direction: Direction, + distance: float, + speed: Optional[float] = None, + acceleration: Optional[float] = None, + current: Optional[float] = None, + ) -> bool: + """Move the axis in a direction by the given distance in mm.""" + motion_params = STACKER_MOTION_CONFIG[axis]["move"] + await self._driver.set_run_current(axis, current or motion_params.current or 0) + if any([speed, acceleration]): + motion_params.max_speed = speed or motion_params.max_speed + motion_params.acceleration = acceleration or motion_params.acceleration + distance = direction.distance(distance) + success = await self._driver.move_in_mm(axis, distance, params=motion_params) + # TODO: This can return a stall, handle that here + return success == MoveResult.NO_ERROR + + async def home_axis( + self, + axis: StackerAxis, + direction: Direction, + speed: Optional[float] = None, + acceleration: Optional[float] = None, + current: Optional[float] = None, + ) -> bool: + motion_params = STACKER_MOTION_CONFIG[axis]["home"] + await self._driver.set_run_current(axis, current or motion_params.current or 0) + # Set the max hold current for the Z axis + if axis == StackerAxis.Z: + await self._driver.set_ihold_current(axis, 1.8) + if any([speed, acceleration]): + motion_params.max_speed = speed or motion_params.max_speed + motion_params.acceleration = acceleration or motion_params.acceleration + success = await self._driver.move_to_limit_switch( + axis=axis, direction=direction, params=motion_params + ) + # TODO: This can return a stall, handle that here + return success == MoveResult.NO_ERROR + + async def close_latch( + self, + velocity: Optional[float] = None, + acceleration: Optional[float] = None, + ) -> bool: + """Close the latch, dropping any labware its holding.""" + # Dont move the latch if its already closed. + if self.limit_switch_status[StackerAxis.L] == StackerAxisState.EXTENDED: + return True + motion_params = STACKER_MOTION_CONFIG[StackerAxis.L]["move"] + speed = velocity or motion_params.max_speed + accel = acceleration or motion_params.acceleration + success = await self.move_axis( + StackerAxis.L, + Direction.RETRACT, + distance=LATCH_TRAVEL, + speed=speed, + acceleration=accel, + ) + # Check that the latch is closed. + await self._reader.get_limit_switch_status() + return ( + success + and self.limit_switch_status[StackerAxis.L] == StackerAxisState.EXTENDED + ) + + async def open_latch( + self, + velocity: Optional[float] = None, + acceleration: Optional[float] = None, + ) -> bool: + """Open the latch.""" + # Dont move the latch if its already opened. + if self.limit_switch_status[StackerAxis.L] == StackerAxisState.RETRACTED: + return True + motion_params = STACKER_MOTION_CONFIG[StackerAxis.L]["move"] + speed = velocity or motion_params.max_speed + accel = acceleration or motion_params.acceleration + success = await self.move_axis( + StackerAxis.L, + Direction.EXTENT, + distance=LATCH_TRAVEL, + speed=speed, + acceleration=accel, + ) + # Check that the latch is opened. + await self._reader.get_limit_switch_status() + return ( + success + and self.limit_switch_status[StackerAxis.L] == StackerAxisState.RETRACTED + ) + + # NOTE: We are defining the interface, will implement in seperate pr. + async def dispense(self) -> bool: + """Dispenses the next labware in the stacker.""" + return True + + async def store(self) -> bool: + """Stores a labware in the stacker.""" + return True + + +class FlexStackerReader(Reader): + error: Optional[str] + + def __init__(self, driver: AbstractFlexStackerDriver) -> None: + self.error: Optional[str] = None + self._driver = driver + self.limit_switch_status = { + axis: StackerAxisState.UNKNOWN for axis in StackerAxis + } + self.platform_state = PlatformState.UNKNOWN + self.hopper_door_closed = False + self.motion_params = {axis: MoveParams(axis=axis) for axis in StackerAxis} + self.get_config = True + + async def read(self) -> None: + await self.get_limit_switch_status() + await self.get_platform_sensor_state() + await self.get_door_closed() + if self.get_config: + await self.get_motion_parameters() + self.get_config = False + self._set_error(None) + + async def get_limit_switch_status(self) -> None: + """Get the limit switch status.""" + status = await self._driver.get_limit_switches_status() + self.limit_switch_status = { + StackerAxis.X: StackerAxisState.from_status(status, StackerAxis.X), + StackerAxis.Z: StackerAxisState.from_status(status, StackerAxis.Z), + StackerAxis.L: StackerAxisState.from_status(status, StackerAxis.L), + } + + async def get_motion_parameters(self) -> None: + """Get the motion parameters used by the axis motors.""" + self.move_params = { + axis: self._driver.get_motion_params(axis) for axis in StackerAxis + } + + async def get_platform_sensor_state(self) -> None: + """Get the platform state.""" + status = await self._driver.get_platform_status() + self.platform_state = PlatformState.from_status(status) + + async def get_door_closed(self) -> None: + """Check if the hopper door is closed.""" + self.hopper_door_closed = await self._driver.get_hopper_door_closed() + + def on_error(self, exception: Exception) -> None: + self._set_error(exception) + + def _set_error(self, exception: Optional[Exception]) -> None: + if exception is None: + self.error = None + else: + try: + self.error = str(exception.args[0]) + except Exception: + self.error = repr(exception) diff --git a/api/src/opentrons/hardware_control/modules/types.py b/api/src/opentrons/hardware_control/modules/types.py index 9b7c33058d4..a43c7046a6b 100644 --- a/api/src/opentrons/hardware_control/modules/types.py +++ b/api/src/opentrons/hardware_control/modules/types.py @@ -17,6 +17,11 @@ from typing_extensions import TypedDict from pathlib import Path +from opentrons.drivers.flex_stacker.types import ( + LimitSwitchStatus, + PlatformStatus, + StackerAxis, +) from opentrons.drivers.rpi_drivers.types import USBPort if TYPE_CHECKING: @@ -27,6 +32,7 @@ HeaterShakerModuleType, MagneticBlockType, AbsorbanceReaderType, + FlexStackerModuleType, ) @@ -62,6 +68,7 @@ class ModuleType(str, Enum): HEATER_SHAKER: HeaterShakerModuleType = "heaterShakerModuleType" MAGNETIC_BLOCK: MagneticBlockType = "magneticBlockType" ABSORBANCE_READER: AbsorbanceReaderType = "absorbanceReaderType" + FLEX_STACKER: FlexStackerModuleType = "flexStackerModuleType" @classmethod def from_model(cls, model: ModuleModel) -> ModuleType: @@ -77,6 +84,8 @@ def from_model(cls, model: ModuleModel) -> ModuleType: return cls.MAGNETIC_BLOCK if isinstance(model, AbsorbanceReaderModel): return cls.ABSORBANCE_READER + if isinstance(model, FlexStackerModuleModel): + return cls.FLEX_STACKER @classmethod def to_module_fixture_id(cls, module_type: ModuleType) -> str: @@ -91,6 +100,8 @@ def to_module_fixture_id(cls, module_type: ModuleType) -> str: return "magneticBlockV1" if module_type == ModuleType.ABSORBANCE_READER: return "absorbanceReaderV1" + if module_type == ModuleType.FLEX_STACKER: + return "flexStackerModuleV1" else: raise ValueError( f"Module Type {module_type} does not have a related fixture ID." @@ -124,6 +135,10 @@ class AbsorbanceReaderModel(str, Enum): ABSORBANCE_READER_V1: str = "absorbanceReaderV1" +class FlexStackerModuleModel(str, Enum): + FLEX_STACKER_V1: str = "flexStackerModuleV1" + + def module_model_from_string(model_string: str) -> ModuleModel: for model_enum in { MagneticModuleModel, @@ -132,6 +147,7 @@ def module_model_from_string(model_string: str) -> ModuleModel: HeaterShakerModuleModel, MagneticBlockModel, AbsorbanceReaderModel, + FlexStackerModuleModel, }: try: return cast(ModuleModel, model_enum(model_string)) @@ -184,6 +200,7 @@ class ModuleInfo(NamedTuple): HeaterShakerModuleModel, MagneticBlockModel, AbsorbanceReaderModel, + FlexStackerModuleModel, ] @@ -225,3 +242,71 @@ class LidStatus(str, Enum): OFF = "off" UNKNOWN = "unknown" ERROR = "error" + + +class FlexStackerStatus(str, Enum): + IDLE = "idle" + DISPENSING = "dispensing" + STORING = "storing" + ERROR = "error" + + +class PlatformState(str, Enum): + UNKNOWN = "unknown" + EXTENDED = "extended" + RETRACTED = "retracted" + + @classmethod + def from_status(cls, status: PlatformStatus) -> "PlatformState": + """Get the state from the platform status.""" + if status.E and not status.R: + return cls.EXTENDED + if status.R and not status.E: + return cls.RETRACTED + return cls.UNKNOWN + + +class StackerAxisState(str, Enum): + UNKNOWN = "unknown" + EXTENDED = "extended" + RETRACTED = "retracted" + + @classmethod + def from_status( + cls, status: LimitSwitchStatus, axis: StackerAxis + ) -> "StackerAxisState": + """Get the axis state from the limit switch status.""" + match axis: + case StackerAxis.X: + if status.XE and not status.XR: + return cls.EXTENDED + if status.XR and not status.XE: + return cls.RETRACTED + case StackerAxis.Z: + if status.ZE and not status.ZR: + return cls.EXTENDED + if status.ZR and not status.ZE: + return cls.RETRACTED + case StackerAxis.L: + return cls.EXTENDED if status.LR else cls.RETRACTED + return cls.UNKNOWN + + +class LatchState(str, Enum): + CLOSED = "closed" + OPENED = "opened" + + @classmethod + def from_state(cls, state: StackerAxisState) -> "LatchState": + """Get the latch state from the axis state.""" + return cls.CLOSED if state == StackerAxisState.EXTENDED else cls.OPENED + + +class HopperDoorState(str, Enum): + CLOSED = "closed" + OPENED = "opened" + + @classmethod + def from_state(cls, state: bool) -> "HopperDoorState": + """Get the hopper door state from the door state boolean.""" + return cls.CLOSED if state else cls.OPENED diff --git a/api/src/opentrons/hardware_control/modules/utils.py b/api/src/opentrons/hardware_control/modules/utils.py index 296c89a1311..481d0366324 100644 --- a/api/src/opentrons/hardware_control/modules/utils.py +++ b/api/src/opentrons/hardware_control/modules/utils.py @@ -13,6 +13,7 @@ from .thermocycler import Thermocycler from .heater_shaker import HeaterShaker from .absorbance_reader import AbsorbanceReader +from .flex_stacker import FlexStacker log = logging.getLogger(__name__) @@ -26,6 +27,7 @@ Thermocycler.name(): Thermocycler.MODULE_TYPE, HeaterShaker.name(): HeaterShaker.MODULE_TYPE, AbsorbanceReader.name(): AbsorbanceReader.MODULE_TYPE, + FlexStacker.name(): FlexStacker.MODULE_TYPE, } _MODULE_CLS_BY_TYPE: Dict[ModuleType, Type[AbstractModule]] = { @@ -34,6 +36,7 @@ Thermocycler.MODULE_TYPE: Thermocycler, HeaterShaker.MODULE_TYPE: HeaterShaker, AbsorbanceReader.MODULE_TYPE: AbsorbanceReader, + FlexStacker.MODULE_TYPE: FlexStacker, } diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 9d596adbaa8..3815077464d 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -476,6 +476,7 @@ class ModuleModel(str, Enum): HEATER_SHAKER_MODULE_V1 = "heaterShakerModuleV1" MAGNETIC_BLOCK_V1 = "magneticBlockV1" ABSORBANCE_READER_V1 = "absorbanceReaderV1" + FLEX_STACKER_MODULE_V1 = "flexStackerModuleV1" def as_type(self) -> ModuleType: """Get the ModuleType of this model.""" @@ -491,6 +492,8 @@ def as_type(self) -> ModuleType: return ModuleType.MAGNETIC_BLOCK elif ModuleModel.is_absorbance_reader(self): return ModuleType.ABSORBANCE_READER + elif ModuleModel.is_flex_stacker(self): + return ModuleType.FLEX_STACKER assert False, f"Invalid ModuleModel {self}" @@ -534,6 +537,11 @@ def is_absorbance_reader( """Whether a given model is an Absorbance Plate Reader.""" return model == cls.ABSORBANCE_READER_V1 + @classmethod + def is_flex_stacker(cls, model: ModuleModel) -> TypeGuard[AbsorbanceReaderModel]: + """Whether a given model is a Flex Stacker..""" + return model == cls.FLEX_STACKER_MODULE_V1 + TemperatureModuleModel = Literal[ ModuleModel.TEMPERATURE_MODULE_V1, ModuleModel.TEMPERATURE_MODULE_V2 @@ -547,6 +555,7 @@ def is_absorbance_reader( HeaterShakerModuleModel = Literal[ModuleModel.HEATER_SHAKER_MODULE_V1] MagneticBlockModel = Literal[ModuleModel.MAGNETIC_BLOCK_V1] AbsorbanceReaderModel = Literal[ModuleModel.ABSORBANCE_READER_V1] +FlexStackerModuleModel = Literal[ModuleModel.FLEX_STACKER_MODULE_V1] class ModuleDimensions(BaseModel): diff --git a/api/tests/opentrons/drivers/flex_stacker/test_driver.py b/api/tests/opentrons/drivers/flex_stacker/test_driver.py index aea2492cf9e..1de13c569cb 100644 --- a/api/tests/opentrons/drivers/flex_stacker/test_driver.py +++ b/api/tests/opentrons/drivers/flex_stacker/test_driver.py @@ -66,6 +66,27 @@ async def test_stop_motors(subject: FlexStackerDriver, connection: AsyncMock) -> await subject.get_device_info() +async def test_get_motion_params( + subject: FlexStackerDriver, connection: AsyncMock +) -> None: + """It should send a get motion params command.""" + connection.send_command.return_value = "M120 M:X V:200.000 A:1500.000 D:5.000" + response = await subject.get_motion_params(types.StackerAxis.X) + assert response == types.MoveParams( + axis=types.StackerAxis.X, + acceleration=1500.0, + max_speed=200.0, + max_speed_discont=5.0, + ) + + command = types.GCODE.GET_MOVE_PARAMS.build_command().add_element( + types.StackerAxis.X.name + ) + response = await connection.send_command(command) + connection.send_command.assert_any_call(command) + connection.reset_mock() + + async def test_set_serial_number( subject: FlexStackerDriver, connection: AsyncMock ) -> None: @@ -94,6 +115,32 @@ async def test_set_serial_number( connection.reset_mock() +async def test_enable_motors(subject: FlexStackerDriver, connection: AsyncMock) -> None: + """It should send a enable motors command""" + connection.send_command.return_value = "M17" + response = await subject.enable_motors([types.StackerAxis.X]) + assert response + + move_to = types.GCODE.ENABLE_MOTORS.build_command().add_element( + types.StackerAxis.X.value + ) + connection.send_command.assert_any_call(move_to) + connection.reset_mock() + + # Test no arg to disable all motors + response = await subject.enable_motors(list(types.StackerAxis)) + assert response + + move_to = types.GCODE.ENABLE_MOTORS.build_command() + move_to.add_element(types.StackerAxis.X.value) + move_to.add_element(types.StackerAxis.Z.value) + move_to.add_element(types.StackerAxis.L.value) + + print("MOVE TO", move_to) + connection.send_command.assert_any_call(move_to) + connection.reset_mock() + + async def test_get_limit_switch( subject: FlexStackerDriver, connection: AsyncMock ) -> None: diff --git a/api/tests/opentrons/hardware_control/modules/test_hc_flexstacker.py b/api/tests/opentrons/hardware_control/modules/test_hc_flexstacker.py new file mode 100644 index 00000000000..0a6d43e441f --- /dev/null +++ b/api/tests/opentrons/hardware_control/modules/test_hc_flexstacker.py @@ -0,0 +1,52 @@ +import asyncio +import pytest +import mock +from typing import AsyncGenerator +from opentrons.hardware_control import modules, ExecutionManager +from opentrons.drivers.rpi_drivers.types import USBPort + + +@pytest.fixture +def usb_port() -> USBPort: + return USBPort( + name="", + port_number=0, + device_path="/dev/ot_module_sim_flexstacker0", + ) + + +@pytest.fixture +async def simulating_module( + usb_port: USBPort, +) -> AsyncGenerator[modules.AbstractModule, None]: + module = await modules.build( + port=usb_port.device_path, + usb_port=usb_port, + type=modules.ModuleType["FLEX_STACKER"], + simulating=True, + hw_control_loop=asyncio.get_running_loop(), + execution_manager=ExecutionManager(), + ) + assert isinstance(module, modules.AbstractModule) + try: + yield module + finally: + await module.cleanup() + + +@pytest.fixture +async def simulating_module_driver_patched( + simulating_module: modules.FlexStacker, +) -> AsyncGenerator[modules.AbstractModule, None]: + driver_mock = mock.MagicMock() + with mock.patch.object( + simulating_module, "_driver", driver_mock + ), mock.patch.object(simulating_module._reader, "_driver", driver_mock): + yield simulating_module + + +async def test_sim_state(simulating_module: modules.FlexStacker) -> None: + status = simulating_module.device_info + assert status["serial"] == "dummySerialFS" + assert status["model"] == "a1" + assert status["version"] == "stacker-fw" diff --git a/api/tests/opentrons/hardware_control/test_modules.py b/api/tests/opentrons/hardware_control/test_modules.py index 5df0b142e07..f5fa0e69336 100644 --- a/api/tests/opentrons/hardware_control/test_modules.py +++ b/api/tests/opentrons/hardware_control/test_modules.py @@ -8,6 +8,7 @@ from opentrons.hardware_control import ExecutionManager from opentrons.hardware_control.modules import ModuleAtPort +from opentrons.hardware_control.modules.flex_stacker import FlexStacker from opentrons.hardware_control.modules.types import ( BundledFirmware, ModuleModel, @@ -16,6 +17,7 @@ HeaterShakerModuleModel, ThermocyclerModuleModel, AbsorbanceReaderModel, + FlexStackerModuleModel, ModuleType, ) from opentrons.hardware_control.modules import ( @@ -49,6 +51,9 @@ async def test_get_modules_simulating() -> None: "absorbancereader": [ SimulatingModule(serial_number="555", model="absorbanceReaderV1") ], + "flexstacker": [ + SimulatingModule(serial_number="656", model="flexStackerModuleV1") + ], } api = await hardware_control.API.build_hardware_simulator(attached_modules=mods) await asyncio.sleep(0.05) @@ -110,6 +115,7 @@ async def test_module_caching() -> None: (ThermocyclerModuleModel.THERMOCYCLER_V1, Thermocycler), (HeaterShakerModuleModel.HEATER_SHAKER_V1, HeaterShaker), (AbsorbanceReaderModel.ABSORBANCE_READER_V1, AbsorbanceReader), + (FlexStackerModuleModel.FLEX_STACKER_V1, FlexStacker), ], ) async def test_create_simulating_module( @@ -259,7 +265,28 @@ async def mod_absorbancereader() -> AsyncIterator[AbstractModule]: await absorbancereader.cleanup() -async def test_module_update_integration( +@pytest.fixture +async def mod_flexstacker() -> AsyncIterator[AbstractModule]: + usb_port = USBPort( + name="", + hub=False, + port_number=0, + device_path="/dev/ot_module_sim_flexstacker0", + ) + + flexstacker = await build_module( + port="/dev/ot_module_sim_flexstacker0", + usb_port=usb_port, + type=ModuleType.FLEX_STACKER, + simulating=True, + hw_control_loop=asyncio.get_running_loop(), + execution_manager=ExecutionManager(), + ) + yield flexstacker + await flexstacker.cleanup() + + +async def test_module_update_integration( # noqa: C901 monkeypatch: pytest.MonkeyPatch, mod_tempdeck: AbstractModule, mod_magdeck: AbstractModule, @@ -267,6 +294,7 @@ async def test_module_update_integration( mod_heatershaker: AbstractModule, mod_thermocycler_gen2: AbstractModule, mod_absorbancereader: AbstractModule, + mod_flexstacker: AbstractModule, ) -> None: from opentrons.hardware_control import modules @@ -362,6 +390,7 @@ async def mock_find_dfu_device_tc2(pid: str, expected_device_count: int) -> str: upload_via_dfu_mock.assert_called_once_with( "df11", "fake_fw_file_path", bootloader_kwargs ) + upload_via_dfu_mock.reset_mock() # Test absorbancereader update with byonoy library bootloader_kwargs["module"] = mod_absorbancereader @@ -373,6 +402,20 @@ async def mock_find_dfu_device_tc2(pid: str, expected_device_count: int) -> str: byonoy_update_firmware_mock.assert_called_once_with("fake_fw_file_path") assert not mod_absorbancereader.updating + # test flex stacker update with dfu bootloader + async def mock_find_dfu_device_fs2(pid: str, expected_device_count: int) -> str: + if expected_device_count == 3: + return "df11" + return "none" + + monkeypatch.setattr(modules.update, "find_dfu_device", mock_find_dfu_device_fs2) + + bootloader_kwargs["module"] = mod_flexstacker + await modules.update_firmware(mod_flexstacker, "fake_fw_file_path") + upload_via_dfu_mock.assert_called_once_with( + "df11", "fake_fw_file_path", bootloader_kwargs + ) + async def test_get_bundled_fw(monkeypatch: pytest.MonkeyPatch, tmpdir: Path) -> None: from opentrons.hardware_control import modules @@ -392,6 +435,9 @@ async def test_get_bundled_fw(monkeypatch: pytest.MonkeyPatch, tmpdir: Path) -> dummy_abs_file = Path(tmpdir) / "absorbance-96@v1.0.2.byoup" dummy_abs_file.write_text("hello") + dummy_fs_file = Path(tmpdir) / "flex-stacker@v7.0.0.bin" + dummy_fs_file.write_text("hello") + dummy_bogus_file = Path(tmpdir) / "thermoshaker@v6.6.6.bin" dummy_bogus_file.write_text("hello") @@ -414,6 +460,9 @@ async def test_get_bundled_fw(monkeypatch: pytest.MonkeyPatch, tmpdir: Path) -> "absorbancereader": [ SimulatingModule(serial_number="555", model="absorbanceReaderV1") ], + "flexstacker": [ + SimulatingModule(serial_number="656", model="flexStackerModuleV1") + ], } api = await API.build_hardware_simulator(attached_modules=mods) @@ -434,6 +483,9 @@ async def test_get_bundled_fw(monkeypatch: pytest.MonkeyPatch, tmpdir: Path) -> assert api.attached_modules[4].bundled_fw == BundledFirmware( version="1.0.2", path=dummy_abs_file ) + assert api.attached_modules[5].bundled_fw == BundledFirmware( + version="7.0.0", path=dummy_fs_file + ) for m in api.attached_modules: await m.cleanup() diff --git a/robot-server/robot_server/modules/module_data_mapper.py b/robot-server/robot_server/modules/module_data_mapper.py index f0c623fdab3..b425c547c22 100644 --- a/robot-server/robot_server/modules/module_data_mapper.py +++ b/robot-server/robot_server/modules/module_data_mapper.py @@ -12,6 +12,9 @@ HeaterShakerStatus, SpeedStatus, AbsorbanceReaderStatus, + PlatformState, + StackerAxisState, + FlexStackerStatus, ) from opentrons.hardware_control.modules.magdeck import OFFSET_TO_LABWARE_BOTTOM from opentrons.drivers.types import ( @@ -22,12 +25,15 @@ ) from opentrons.drivers.rpi_drivers.types import USBPort as HardwareUSBPort +from opentrons.hardware_control.modules.types import HopperDoorState, LatchState from opentrons.protocol_engine import ModuleModel, DeckType from .module_identifier import ModuleIdentity from .module_models import ( AttachedModule, AttachedModuleData, + FlexStackerModule, + FlexStackerModuleData, MagneticModule, MagneticModuleData, ModuleCalibrationData, @@ -155,6 +161,20 @@ def map_data( int, live_data["data"].get("referenceWavelength") ), ) + elif module_type == ModuleType.FLEX_STACKER: + module_cls = FlexStackerModule + module_data = FlexStackerModuleData( + status=FlexStackerStatus(live_data["status"]), + latchState=cast(LatchState, live_data["data"].get("latchState")), + platformState=cast( + PlatformState, live_data["data"].get("platformState") + ), + hopperDoorState=cast( + HopperDoorState, live_data["data"].get("hopperDoorState") + ), + axisStateX=cast(StackerAxisState, live_data["data"].get("axisStateX")), + axisStateZ=cast(StackerAxisState, live_data["data"].get("axisStateZ")), + ) else: assert False, f"Invalid module type {module_type}" diff --git a/robot-server/robot_server/modules/module_models.py b/robot-server/robot_server/modules/module_models.py index 7b6da4925ac..fb56e9e3fbf 100644 --- a/robot-server/robot_server/modules/module_models.py +++ b/robot-server/robot_server/modules/module_models.py @@ -12,6 +12,8 @@ HeaterShakerStatus, SpeedStatus, AbsorbanceReaderStatus, + PlatformState, + StackerAxisState, ) from opentrons.drivers.types import ( ThermocyclerLidStatus, @@ -19,6 +21,7 @@ AbsorbanceReaderLidStatus, AbsorbanceReaderPlatePresence, ) +from opentrons.hardware_control.modules.types import HopperDoorState, LatchState from opentrons.protocol_engine import ModuleModel from opentrons.protocol_engine.types import Vec3f @@ -343,19 +346,54 @@ class AbsorbanceReaderModule( AbsorbanceReaderModuleData, ] ): - """An attached Heater-Shaker Module.""" + """An attached Absorbance Reader Module.""" moduleType: Literal[ModuleType.ABSORBANCE_READER] moduleModel: Literal[ModuleModel.ABSORBANCE_READER_V1] data: AbsorbanceReaderModuleData +class FlexStackerModuleData(BaseModel): + """Live data from a Flex Stacker module.""" + + status: str = Field( + ..., + description="Overall status of the module.", + ) + latchState: LatchState = Field(..., description="The state of the labware latch.") + platformState: PlatformState = Field(..., description="The state of the platform.") + hopperDoorState: HopperDoorState = Field( + ..., description="The state of the hopper door." + ) + axisStateX: StackerAxisState = Field( + ..., description="The state of the X axis limit switches." + ) + axisStateZ: StackerAxisState = Field( + ..., description="The state of the Z axis limit switches." + ) + + +class FlexStackerModule( + _GenericModule[ + Literal[ModuleType.FLEX_STACKER], + Literal[ModuleModel.FLEX_STACKER_MODULE_V1], + FlexStackerModuleData, + ] +): + """An attached Flex Stacker Module.""" + + moduleType: Literal[ModuleType.FLEX_STACKER] + moduleModel: Literal[ModuleModel.FLEX_STACKER_MODULE_V1] + data: FlexStackerModuleData + + AttachedModule = Union[ TemperatureModule, MagneticModule, ThermocyclerModule, HeaterShakerModule, AbsorbanceReaderModule, + FlexStackerModule, ] @@ -365,4 +403,5 @@ class AbsorbanceReaderModule( ThermocyclerModuleData, HeaterShakerModuleData, AbsorbanceReaderModuleData, + FlexStackerModuleData, ] diff --git a/shared-data/module/definitions/3/flexStackerModuleV1.json b/shared-data/module/definitions/3/flexStackerModuleV1.json new file mode 100644 index 00000000000..fa6f7db3494 --- /dev/null +++ b/shared-data/module/definitions/3/flexStackerModuleV1.json @@ -0,0 +1,524 @@ +{ + "$otSharedSchema": "module/schemas/3", + "moduleType": "flexStackerModuleType", + "model": "flexStackerModuleV1", + "labwareOffset": { + "x": -0.125, + "y": 1.125, + "z": 68.275 + }, + "dimensions": { + "bareOverallHeight": 82, + "overLabwareHeight": 0, + "xDimension": 156.25, + "yDimension": 91.75, + "footprintXDimension": 128, + "footprintYDimension": 86, + "labwareInterfaceXDimension": 128, + "labwareInterfaceYDimension": 86 + }, + "cornerOffsetFromSlot": { + "x": -18, + "y": -1.8, + "z": 0 + }, + "calibrationPoint": { + "x": 12.0, + "y": 8.75, + "z": 68.275 + }, + "gripperOffsets": { + "default": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 0, + "y": 0, + "z": 1.0 + } + } + }, + "displayName": "Flex Stacker Module GEN1", + "quirks": [], + "slotTransforms": { + "ot2_standard": { + "3": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "6": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "9": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + } + }, + "ot2_short_trash": { + "3": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "6": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + }, + "9": { + "labwareOffset": [ + [-1, 0, 0, 0], + [0, -1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1] + ] + } + }, + "ot3_standard": { + "D1": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "C1": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "B1": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "A1": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "D3": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "C3": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "B3": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + }, + "A3": { + "labwareOffset": [ + [1, 0, 0, 0.125], + [0, 1, 0, -1.125], + [0, 0, 1, -49.325], + [0, 0, 0, 1] + ] + } + } + }, + "compatibleWith": [], + "incompatibleWithDecks": [], + "twoDimensionalRendering": { + "name": "svg", + "type": "element", + "value": "", + "attributes": { + "version": "1.1", + "id": "flexstacker", + "xmlns": "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink", + "x": "0px", + "y": "0px", + "viewBox": "0 0 148.6 91.8", + "style": "enable-background:new 0 0 148.6 91.8;", + "xml:space": "preserve" + }, + "children": [ + { + "name": "g", + "type": "element", + "value": "", + "attributes": { + "id": "moduleBaseFill" + }, + "children": [ + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "style": "fill:#E6E6E6;", + "d": "M24.4,88.6c0.9,0,1.1-0.7,1.1-1.4v-3.4c0-0.2-0.2-0.5-0.5-0.5h-5.2c-0.2,0-0.5-0.2-0.5-0.5v-4.5\n\t\tc0-0.2-0.2-0.5-0.5-0.5h-5.7c-1,0-1.9-0.8-1.9-1.9V15.6c0-1,0.8-1.9,1.9-1.9h5.7c0.2,0,0.5-0.2,0.5-0.5V8.8c0-0.2,0.2-0.5,0.5-0.5\n\t\tH25c0.2,0,0.5-0.2,0.5-0.5V4.5c0-0.8-0.4-1.4-1.1-1.4s-23.9,0-23.9,0v85.5C0.5,88.6,23.5,88.6,24.4,88.6z" + }, + "children": [] + } + ] + }, + { + "name": "g", + "type": "element", + "value": "", + "attributes": { + "id": "moduleOutline" + }, + "children": [ + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "d": "M147.9,1.1v89.6H1.1V1.1H147.9 M148.9,0H0v91.7h148.9V0L148.9,0z" + }, + "children": [] + } + ] + }, + { + "name": "g", + "type": "element", + "value": "", + "attributes": { + "id": "moduleBaseInnerWalls" + }, + "children": [ + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "d": "M148.4,89.1H146v-1.4H26c-0.2,1.2-1.2,1.4-1.6,1.4H0.5v-1h23.8c0.3,0,0.7-0.1,0.7-0.9v-0.5h122v1.4h1.4V89.1z" + }, + "children": [] + }, + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "d": "M147,5H25V4.5c0-0.8-0.3-0.9-0.7-0.9H0.5v-1h23.8c0.4,0,1.4,0.1,1.6,1.4h120V2.6h2.4v1H147V5z" + }, + "children": [] + } + ] + }, + { + "name": "g", + "type": "element", + "value": "", + "attributes": { + "id": "statusFill" + }, + "children": [ + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "style": "fill:#FFFFFF;", + "d": "M4.8,40.3c0-1.1,0.9-2,2-2s2,0.9,2,2v11.2c0,1.1-0.9,2-2,2s-2-0.9-2-2V40.3z" + }, + "children": [] + } + ] + }, + { + "name": "g", + "type": "element", + "value": "", + "attributes": { + "id": "statusOutline" + }, + "children": [ + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "d": "M6.8,53.7H6.7c-1.2,0-2.2-1-2.2-2.2V40.2c0-1.2,1-2.2,2.2-2.2h0.1C8,38,9,39,9,40.2v11.3C9,52.7,8,53.7,6.8,53.7z\n\t\t M6.7,38.5c-0.9,0-1.7,0.8-1.7,1.7v11.3c0,1,0.8,1.7,1.7,1.7h0.1c1,0,1.7-0.8,1.7-1.7V40.2c0-1-0.8-1.7-1.7-1.7H6.7z" + }, + "children": [] + } + ] + }, + { + "name": "g", + "type": "element", + "value": "", + "attributes": { + "id": "moduleMidFill" + }, + "children": [ + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "style": "fill:#E6E6E6;", + "d": "M12.1,91.2L8.6,86c-0.1-0.1-0.1-0.2-0.1-0.3V6c0-0.1,0-0.2,0.1-0.3l3.5-5.2h134.2l3.5,5.2\n\t\tc0.1,0.1,0.1,0.2,0.1,0.3v79.7c0,0.1,0,0.2-0.1,0.3l-3.5,5.2H12.1" + }, + "children": [] + } + ] + }, + { + "name": "g", + "type": "element", + "value": "", + "attributes": { + "id": "moduleMidOutline" + }, + "children": [ + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "d": "M146,1.1l3.3,5v79.7l-3.3,5H12.3l-3.3-5V6l3.3-4.9L146,1.1 M146.5,0H11.8L8.2,5.4C8.1,5.6,8,5.8,8,6v79.7\n\t\tc0,0.2,0.1,0.4,0.2,0.6l3.6,5.4h134.8l3.6-5.4c0.1-0.2,0.2-0.4,0.2-0.6V6c0-0.2-0.1-0.4-0.2-0.6L146.5,0L146.5,0z" + }, + "children": [] + } + ] + }, + { + "name": "g", + "type": "element", + "value": "", + "attributes": { + "id": "plateBottomFill" + }, + "children": [ + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "style": "fill:#FFFFFF;", + "d": "M49.1,6.5c-0.9,0-1.7,0.6-1.9,1.6c-0.4,1.7-1.9,2.9-3.7,2.9s-3.3-1.2-3.7-2.9\n\t\tc-0.2-1-1.1-1.6-1.9-1.6H24.4c-1.6,0-2.9,1.3-2.9,2.9v72.9c0,1.6,1.3,2.9,2.9,2.9h13.4c0.8,0,1.6-0.6,1.9-1.6\n\t\tc0.4-1.7,1.9-2.9,3.7-2.9s3.3,1.2,3.7,2.9c0.2,1,1,1.6,1.9,1.6h60.2c0.9,0,1.7-0.6,1.9-1.6c0.4-1.7,1.9-2.9,3.7-2.9\n\t\tc1.8,0,3.3,1.2,3.7,2.9c0.2,1,1.1,1.6,1.9,1.6H134c1.6,0,2.9-1.3,2.9-2.9V9.4c0-1.6-1.3-2.9-2.9-2.9h-13.4c-0.8,0-1.6,0.6-1.9,1.6\n\t\tc-0.4,1.7-1.9,2.9-3.7,2.9c-1.8,0-3.3-1.2-3.7-2.9c-0.2-1-1-1.6-1.9-1.6L49.1,6.5L49.1,6.5z" + }, + "children": [] + } + ] + }, + { + "name": "g", + "type": "element", + "value": "", + "attributes": { + "id": "plateBottomOutline" + }, + "children": [ + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "d": "M134,85.5h-13.4c-1,0-1.8-0.7-2.1-1.7c-0.4-1.6-1.8-2.7-3.5-2.7s-3.1,1.1-3.5,2.7c-0.3,1.1-1.1,1.7-2.1,1.7H49.1\n\t\tc-1,0-1.9-0.7-2.1-1.7c-0.4-1.6-1.8-2.7-3.5-2.7s-3.1,1.1-3.5,2.7c-0.3,1-1.1,1.7-2.1,1.7H24.4c-1.8,0-3.2-1.4-3.2-3.2V9.4\n\t\tc0-1.8,1.4-3.2,3.2-3.2h13.4c1,0,1.8,0.7,2.1,1.7c0.4,1.6,1.8,2.7,3.5,2.7s3.1-1.1,3.5-2.7c0.3-1,1-1.6,1.9-1.7l0,0h60.4\n\t\tc1,0,1.9,0.7,2.1,1.7c0.4,1.6,1.8,2.7,3.5,2.7s3.1-1.1,3.5-2.7c0.3-1,1.1-1.7,2.1-1.7H134c1.8,0,3.2,1.4,3.2,3.2v72.9\n\t\tC137.1,84.1,135.7,85.5,134,85.5z M114.9,80.6c1.9,0,3.5,1.3,4,3.1c0.2,0.8,0.9,1.4,1.6,1.4H134c1.5,0,2.7-1.2,2.7-2.7v-73\n\t\tc0-1.5-1.2-2.7-2.7-2.7h-13.4c-0.7,0-1.4,0.6-1.6,1.4c-0.5,1.8-2.1,3.1-4,3.1s-3.5-1.3-4-3.1c-0.2-0.8-0.9-1.4-1.7-1.4h-60h-0.2\n\t\tc-0.8,0-1.4,0.5-1.7,1.4c-0.5,1.8-2.1,3.1-4,3.1s-3.5-1.3-4-3.1c-0.2-0.8-0.9-1.4-1.6-1.4H24.4c-1.5,0-2.7,1.2-2.7,2.7v72.9\n\t\tc0,1.5,1.2,2.7,2.7,2.7h13.4c0.7,0,1.4-0.6,1.6-1.4c0.5-1.8,2.1-3.1,4-3.1s3.5,1.3,4,3.1c0.2,0.8,0.9,1.4,1.7,1.4h60.2\n\t\tc0.8,0,1.4-0.5,1.7-1.4C111.4,81.8,113,80.6,114.9,80.6z" + }, + "children": [] + } + ] + }, + { + "name": "g", + "type": "element", + "value": "", + "attributes": { + "id": "plateHolder" + }, + "children": [ + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "d": "M114.9,3c-2.3,0-4.1,1.8-4.1,4.1s1.8,4.1,4.1,4.1s4.1-1.8,4.1-4.1S117.2,3,114.9,3z M111.3,7.1c0-0.4,0.1-0.8,0.2-1.1h6.8\n\t\tc0.1,0.4,0.2,0.7,0.2,1.1c0,0.2,0,0.4-0.1,0.6h-7.1C111.4,7.5,111.3,7.3,111.3,7.1z M114.9,3.5c1.4,0,2.6,0.8,3.2,2h-6.4\n\t\tC112.3,4.3,113.5,3.5,114.9,3.5z M114.9,10.7c-1.6,0-2.9-1.1-3.4-2.5h6.8C117.9,9.6,116.5,10.7,114.9,10.7z" + }, + "children": [] + }, + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "d": "M43.4,3c-2.3,0-4.1,1.8-4.1,4.1s1.8,4.1,4.1,4.1s4.1-1.8,4.1-4.1S45.7,3,43.4,3z M39.8,7.1c0-0.4,0.1-0.8,0.2-1.1h6.8\n\t\tC47,6.3,47,6.7,47,7.1c0,0.2,0,0.4-0.1,0.6h-7.1C39.9,7.5,39.8,7.3,39.8,7.1z M43.4,3.5c1.4,0,2.6,0.8,3.2,2h-6.4\n\t\tC40.9,4.3,42,3.5,43.4,3.5z M43.4,10.7c-1.6,0-2.9-1.1-3.4-2.5h6.8C46.4,9.6,45,10.7,43.4,10.7z" + }, + "children": [] + }, + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "d": "M43.4,80.5c-2.3,0-4.1,1.8-4.1,4.1s1.8,4.1,4.1,4.1s4.1-1.8,4.1-4.1S45.7,80.5,43.4,80.5z M43.4,81c1.6,0,3,1.1,3.4,2.5H40\n\t\tC40.5,82.1,41.8,81,43.4,81z M47,84.6c0,0.4-0.1,0.8-0.2,1.2H40c-0.1-0.4-0.2-0.8-0.2-1.2c0-0.2,0-0.4,0.1-0.6H47\n\t\tC47,84.2,47,84.4,47,84.6z M43.4,88.2c-1.4,0-2.6-0.8-3.2-1.9h6.3C46,87.4,44.8,88.2,43.4,88.2z" + }, + "children": [] + }, + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "d": "M114.9,80.5c-2.3,0-4.1,1.8-4.1,4.1s1.8,4.1,4.1,4.1s4.1-1.8,4.1-4.1S117.2,80.5,114.9,80.5z M114.9,81\n\t\tc1.6,0,3,1.1,3.4,2.5h-6.8C112,82.1,113.3,81,114.9,81z M118.5,84.6c0,0.4-0.1,0.8-0.2,1.2h-6.8c-0.1-0.4-0.2-0.8-0.2-1.2\n\t\tc0-0.2,0-0.4,0.1-0.6h7.1C118.5,84.2,118.5,84.4,118.5,84.6z M114.9,88.2c-1.4,0-2.6-0.8-3.2-1.9h6.3\n\t\tC117.5,87.4,116.3,88.2,114.9,88.2z" + }, + "children": [] + } + ] + }, + { + "name": "g", + "type": "element", + "value": "", + "attributes": { + "id": "adapterScrew" + }, + "children": [ + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "style": "fill:#707075; opacity: 50%;", + "d": "M79.2,50.5c-2.6,0-4.7-2.1-4.7-4.7s2.1-4.7,4.7-4.7s4.7,2.1,4.7,4.7S81.7,50.5,79.2,50.5z M79.2,41.6\n\t\tc-2.3,0-4.2,1.9-4.2,4.2s1.9,4.2,4.2,4.2c2.3,0,4.2-1.9,4.2-4.2C83.4,43.5,81.5,41.6,79.2,41.6z" + }, + "children": [] + }, + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "style": "fill:#707075; opacity: 50%;", + "d": "M79.2,49c-1.8,0-3.2-1.4-3.2-3.2s1.4-3.2,3.2-3.2s3.2,1.4,3.2,3.2C82.3,47.6,80.9,49,79.2,49z M79.2,43.2\n\t\tc-1.5,0-2.7,1.2-2.7,2.7s1.2,2.7,2.7,2.7s2.7-1.2,2.7-2.7C81.8,44.4,80.6,43.2,79.2,43.2z" + }, + "children": [] + } + ] + }, + { + "name": "g", + "type": "element", + "value": "", + "attributes": { + "id": "leftLatchFill" + }, + "children": [ + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "style": "fill:#E6E6E6;", + "d": "M14.1,6.6c0-0.4,0.1-0.8,0.4-1.2L18.8,0h-7L8.2,5.4C8.1,5.6,8,5.8,8,6v79.7c0,0.2,0.1,0.4,0.2,0.6\n\t\tl3.6,5.4h7l-4.3-5.4c-0.2-0.4-0.4-0.7-0.4-1.2L14.1,6.6L14.1,6.6z" + }, + "children": [] + } + ] + }, + { + "name": "g", + "type": "element", + "value": "", + "attributes": { + "id": "leftLatchOutline" + }, + "children": [ + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "d": "M16.6,1.1l-2.9,3.7c-0.4,0.5-0.6,1.2-0.6,1.8v78.5c0,0.7,0.2,1.3,0.6,1.8l2.9,3.7h-4.3l-3.3-5V6l3.3-4.9L16.6,1.1 M18.8,0\n\t\th-7L8.2,5.4C8.1,5.6,8,5.8,8,6v79.7c0,0.2,0.1,0.4,0.2,0.6l3.6,5.4h7l-4.3-5.4c-0.2-0.4-0.4-0.7-0.4-1.2V6.6c0-0.4,0.1-0.8,0.4-1.2\n\t\tL18.8,0L18.8,0z" + }, + "children": [] + } + ] + }, + { + "name": "g", + "type": "element", + "value": "", + "attributes": { + "id": "rightLatchFill" + }, + "children": [ + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "style": "fill:#E6E6E6;", + "d": "M145.3,6.6c0-0.4-0.1-0.8-0.4-1.2L140.6,0h7l3.6,5.4c0.1,0.2,0.2,0.4,0.2,0.6v79.7\n\t\tc0,0.2-0.1,0.4-0.2,0.6l-3.6,5.4h-7l4.3-5.4c0.2-0.4,0.4-0.7,0.4-1.2V6.6z" + }, + "children": [] + } + ] + }, + { + "name": "g", + "type": "element", + "value": "", + "attributes": { + "id": "rightLatchOutline" + }, + "children": [ + { + "name": "path", + "type": "element", + "value": "", + "attributes": { + "d": "M147.1,1.1l3.3,5v79.7l-3.3,5h-4.3l2.9-3.7c0.4-0.5,0.6-1.2,0.6-1.8V6.6c0-0.7-0.2-1.3-0.6-1.8l-2.9-3.7L147.1,1.1\n\t\t M147.6,0h-7l4.3,5.4c0.2,0.4,0.4,0.7,0.4,1.2v78.5c0,0.4-0.1,0.8-0.4,1.2l-4.3,5.4h7l3.6-5.4c0.1-0.2,0.2-0.4,0.2-0.6V5.9\n\t\tc0-0.2-0.1-0.4-0.2-0.6L147.6,0L147.6,0z" + }, + "children": [] + } + ] + } + ] + } +} diff --git a/shared-data/module/definitions/3/flexStackerV1.json b/shared-data/module/definitions/3/flexStackerV1.json deleted file mode 100644 index 8ded657f59b..00000000000 --- a/shared-data/module/definitions/3/flexStackerV1.json +++ /dev/null @@ -1,184 +0,0 @@ -{ - "$otSharedSchema": "module/schemas/3", - "moduleType": "flexStackerType", - "model": "flexStackerV1", - "labwareOffset": { - "x": 0.0, - "y": 0.0, - "z": 0.0 - }, - "dimensions": { - "bareOverallHeight": 18.5, - "overLabwareHeight": 0.0, - "lidHeight": 60.0, - "xDimension": 155.3, - "yDimension": 95.5, - "labwareInterfaceXDimension": 127.8, - "labwareInterfaceYDimension": 85.5 - }, - "cornerOffsetFromSlot": { - "x": -4.875, - "y": -4.875, - "z": 0 - }, - "calibrationPoint": { - "x": 14.4, - "y": 64.93, - "z": 97.8 - }, - "displayName": "FLEX Stacker Module GEN1", - "quirks": [], - "slotTransforms": { - "ot2_standard": {}, - "ot2_short_trash": {}, - "ot3_standard": {} - }, - "compatibleWith": [], - "incompatibleWithDecks": ["ot2_standard", "ot2_short_trash"], - "twoDimensionalRendering": { - "name": "svg", - "type": "element", - "value": "", - "attributes": { - "version": "1.1", - "id": "flexStacker", - "xmlns": "http://www.w3.org/2000/svg", - "xmlns:xlink": "http://www.w3.org/1999/xlink", - "width": "134", - "height": "96", - "viewBox": "0 0 134 96", - "style": "enable-background:new 0 0 148.6 91.8;", - "xml:space": "preserve" - }, - "children": [ - { - "name": "defs", - "type": "element", - "value": "", - "attributes": {}, - "children": [ - { - "name": "style", - "type": "element", - "value": "", - "attributes": {}, - "children": [ - { - "name": "", - "type": "text", - "value": "\n\t\t.cls-1{fill:none;}.cls-1,.cls-2{stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:.71px;}.cls-2{fill:#e6e6e6;}", - "attributes": {}, - "children": [] - } - ] - } - ] - }, - { - "name": "g", - "type": "element", - "value": "", - "attributes": { - "id": "Foot_Print" - }, - "children": [ - { - "name": "rect", - "type": "element", - "value": "", - "attributes": { - "class": "cls-2", - "x": ".35", - "y": ".36", - "width": "136", - "height": "94", - "rx": "5", - "ry": "5" - }, - "children": [] - } - ] - }, - { - "name": "g", - "type": "element", - "value": "", - "attributes": { - "id": "Slot_Clips" - }, - "children": [ - { - "name": "path", - "type": "element", - "value": "", - "attributes": { - "class": "cls-1", - "d": "m17.37.36v2.6m-2-.93V.36m2,2.6c0-.52-.9-.93-2-.93m104,.93V.36m2,1.67c-1.1,0-2,.42-2,.93m2-2.6v1.67m-2,92.33v-2.6m2,.93v1.67m-2-2.6c0,.52.9.93,2,.93M5.37,2.03v1.99m.18.12c-.06-.05-.12-.08-.18-.12m.18.12c.18.14.4.22.63.22m11.19,0H6.18m11.19-1.4v1.4M5.37,2.03h10M6.18,90.35h11.19m-11.19,0c-.23,0-.45.08-.63.22m-.18.12c.06-.03.13-.07.18-.12m-.18,2.11v-1.99m10,1.99H5.37m10,0c1.1,0,2-.42,2-.93m0-1.4v1.4m-15.33-2.4h1.99m.12-.18c-.05.06-.08.12-.12.18m.12-.18c.14-.18.22-.4.22-.63m0-11.19v11.19m-1.4-11.19h1.4m-1.4,0c-.52,0-.93.9-.93,2m0,10v-10m130.33,9.19v-11.19m0,11.19c0,.23.08.45.22.63m.12.18c-.03-.06-.07-.13-.12-.18m2.11.18h-1.99m1.99-10v10m0-10c0-1.1-.42-2-.93-2m-1.4,0h1.4m-2.4,15.33v-1.99m-.18-.12c.06.05.12.08.18.12m-.18-.12c-.18-.14-.4-.22-.63-.22m-11.19,0h11.19m-11.19,1.4v-1.4m12,2.33h-10m9.19-88.33h-11.19m11.19,0c.23,0,.45-.08.63-.22m.18-.12c-.06.03-.13.07-.18.12m.18-2.11v1.99m-10-1.99h10m-12,2.33v-1.4m15.33,2.4h-1.99m-.12.18c.05-.06.08-.12.12-.18m-.12.18c-.14.18-.22.4-.22.63m0,11.19V6.18m1.4,11.19h-1.4m1.4,0c.52,0,.93-.9.93-2m0-10v10M4.37,6.18v11.19m0-11.19c0-.23-.08-.45-.22-.63m-.12-.18c.03.06.07.13.12.18m-2.11-.19h1.99m-1.99,10V5.36m0,10c0,1.1.42,2,.93,2m1.4,0h-1.4m133.4,0h-2.6m.93-2h1.67m-2.6,62h2.6m0,2h-1.67M2.97,17.37H.36m0-2h1.67m15.33,76.4v2.6m-2,0v-1.67M.36,77.35h2.6m-.93,2H.36M134.68,5.36c0-1.84-1.49-3.33-3.33-3.33m0,1.99c.81-.44,1.78.53,1.34,1.34M2.04,89.37c0,1.84,1.49,3.33,3.33,3.33M5.36,2.04c-1.84,0-3.33,1.49-3.33,3.33m129.33,87.33c1.84,0,3.33-1.49,3.33-3.33m-1.99,0c.44.81-.53,1.78-1.34,1.34m-126,0c-.81.44-1.78-.53-1.34-1.34M4.03,5.37c-.44-.81.53-1.78,1.34-1.34m112,90.34v-4m0,0h2M4.37,19.37H.36m4-2v2M.36,75.35h4m0,0v2M136.36,19.37h-4m0,0v-2m0,58h4m-4,2v-2M117.35,4.37V.36m2,4h-2M19.37.36v4m0,0h-2m2,86v4m-2-4h2" - }, - "children": [] - } - ] - }, - { - "name": "g", - "type": "element", - "value": "", - "attributes": { - "id": "Module_Title" - }, - "children": [ - { - "name": "path", - "type": "element", - "value": "", - "attributes": { - "class": "cls-1", - "d": "m73.92,6.86h-.49m0,.5h.49m-.54-.04s.02.04.04.04m2.44-2.71s.04,0,.04-.01m-.36-.03c.11,0,.24.02.32.04m-.78.68c0-.55.1-.72.46-.72m0,1.48c-.35,0-.46-.17-.46-.77m.79.73c-.08.02-.21.04-.33.04m.37-.03s-.01-.02-.04-.02m.05.38v-.36m-.02.39s.02,0,.02-.03m-.48.09c.2,0,.37-.02.46-.06m-1.4-1.13c0,.73.24,1.19.94,1.19m0-2.35c-.69,0-.94.41-.94,1.16m1.42-1.1c-.12-.04-.3-.06-.47-.06m1.83,2.92h.91m-.95-.04s.02.04.04.04m1.48-1.41c.21-.08.41-.25.41-.7m-.06,1.39c0-.39-.16-.5-.32-.6m-.6,1.32c.57,0,.92-.17.92-.72m1.44-2.16c-.09-.02-.19-.04-.32-.04m-24.18,1.46c0,1.45-1.04,2.65-2.42,2.89m-4.39-2.89c0,1.9,1.37,3.48,3.18,3.8m.69-7.67c-2.14,0-3.87,1.73-3.87,3.87m5.13.3h0m31.38-.73v-.95m.07.99c-.06,0-.07-.01-.07-.04m1.4-.95s.01-.05-.05-.05m-.72,1.17c.18-.25.67-.98.77-1.13m-1.41,1.41s.03-.04.08-.04m-1.08.43s-.01-.02-.04-.02m.05.38l-.02-.36m.01-1.83c-.12-.04-.3-.06-.47-.06m-1.67,2.13c.36-.33.35-1.57,0-1.9m-1.45,1.9c.23.3,1.22.3,1.45,0m-22.12-.81l.47,1.56m-.55-1.56s.02-.06.04-.06m-.49,1.61l.45-1.56m-.5,1.6s.04-.02.05-.04m-.8.04h.75m-.79-.04s.02.04.04.04m-.04-2.84v2.79m.04-2.84s-.04.02-.04.04m.52-.04h-.47m.52.04s-.02-.04-.04-.04m0,2.19l.03-2.15m0,2.22s-.02-.01-.02-.06m.07,0s-.02.06-.04.06m.47-1.54l-.43,1.49m.52-1.55s-.06,0-.08.06m.48-.06h-.4m.98,1.61s-.03-.02-.04-.06m.06,0s0,.05-.02.05m0-2.22l.02,2.17m.02-2.21s-.04.02-.04.04m.52-.04h-.48m.52.04s-.02-.04-.04-.04m.04,2.84v-2.79m-.04,2.84s.04-.02.04-.04m-.75.04h.71m-.77-.04s.03.04.06.04m2.52-1.33c0,.25-.12.31-.45.31m.45-.48v.17m-.02-.19l.02.02m-.52-.09l.5.06m-.23-1.4c-.76,0-.99.14-.99.63m1.73-.55c-.15-.05-.45-.08-.74-.08m.8.14s-.01-.05-.06-.06m.06,1.6v-1.53m-1.58,2.13c.16.05.46.08.74.08m-.77-.14s0,.05.02.06m0-.38l-.03.32m.09-.35s-.06,0-.06.02m.59.02c-.15,0-.39-.02-.53-.04m3.92.4l.04-.04m-.82.13c.24,0,.62-.02.79-.09m-1.88-1.04c0,.8.29,1.13,1.09,1.13m-.11-2.17c-.72,0-.98.23-.98,1.03m1.34-1.01c-.11-.01-.23-.02-.36-.02m.39-.01s0,.03-.02.03m-.51-.5c.5,0,.53.13.53.47m-1.19-.39c.16-.04.45-.08.66-.08m.06-.42c-.19,0-.62.05-.78.11m1.8.7c0-.67-.4-.81-1.02-.81m3.45.76h-.47m.51.04s-.02-.04-.04-.04m.04,1.5v-1.46m-.94,2.26c.74,0,.94-.19.94-.81m-1.82.72c.23.04.61.09.87.09m-.92-.13s.02.04.04.04m-.04-2.17v2.13m.04-2.17s-.04.02-.04.04m.51-.04h-.47m.51.04s-.02-.04-.04-.04m.04,1.81v-1.77m.04,1.81s-.04-.02-.04-.04m.34.07c-.13,0-.23,0-.3-.02m.71-.38c0,.32-.07.41-.41.41m.41-1.84v1.43m.04-1.47s-.04.02-.04.04m2.9.08s0-.03-.03-.04m0,.37l.03-.33m-.08.35l.05-.02m-.64-.08c.22,0,.47.06.6.1m-1.21.47c0-.46.22-.57.61-.57m-.59.59l-.02-.02m1.22.02h-1.2m1.31.36c0-.29-.01-.36-.11-.36m-.77,1.34c.67,0,.88-.37.88-.98m-1.89-.2c0,.76.32,1.18,1.01,1.18m.07-2.35c-.83,0-1.09.45-1.09,1.17m1.81-1.04c-.17-.08-.43-.12-.72-.12m2.37.38v-.29m-.04.33s.04,0,.04-.04m-.17.04h.13m-.31.18c0-.14.05-.18.18-.18m-.18,1.43v-1.26m.42,1.32s-.02-.04-.04-.04m.04.39v-.34m-.04.38s.04-.02.04-.04m-.4.04h.35m-.38.02l.02-.02m-.03.55v-.53m-.04.57s.04-.02.04-.04m-.51.04h.47m-.51-.04s.02.04.04.04m-.04-.57v.53m-.02-.55l.02.02m-.32-.02h.29m-.34-.04s.02.04.04.04m-.04-.38v.34m.04-.38s-.04.02-.04.04m.34-.05h-.29m.32-.02l-.02.02m.02-1.41v1.39m.51-1.85c-.34,0-.51.16-.51.47m.85-.43c-.09-.02-.22-.04-.34-.04m.83,2.72v.42m-.77-1.27l-.02-.02m1.35.41v-2.18m-.55,2.18s.02.04.04.04m0-2.26s-.04.02-.04.04m-8.66-.33s.01.03.04.02m-.09-.36l.05.33m-.02-.36l-.03.04m19.59.58c-.69,0-.94.41-.94,1.16m2.41-1.08s-.02-.04-.04-.04m-23.41.59c0,.43.1.62.72.7m24.09-1.29h-.54m-23.33,2.31c.53,0,.84-.18.84-.67m21.74-.21l.08.02m-24.74.77l-.47-1.48m-7.07,3.59l.16.91m19.16-5.17s-.02-.05-.04-.06m9.71.19c-.25-.31-1.22-.31-1.46,0m-.95,2.92s.04-.02.04-.05m-.02-3.1c-.35,0-.53.12-.53.52m-19.25.75s.03.01.04.06m26.08-1.28s-.04,0-.06.04m-7.73,1.52s-.05-.04-.05-.06m-18.04-.78c-.02-.06-.02-.07-.07-.07m15.29-.23l.02-.36m-23.52,5.17l.17-.91m32.19-4.25l-.02-.04m-.01.4l.02-.36m-.07.38s.04,0,.04-.01m-.36-.03c.11,0,.24.02.32.04m-.78.68c0-.55.1-.72.46-.72m0,1.48c-.35,0-.46-.17-.46-.77m.79.73c-.08.02-.21.04-.33.04m.36.36s.02,0,.02-.03m.98.85v-1.61m-.04,1.65s.04-.02.04-.04m-.5.04h.46m-.5-.04s.02.04.04.04m-.04-3.07v3.03m.04-3.07s-.04.02-.04.04m.5-.04h-.46m-11.84.04v2.18m12.52-1.24s-.06.04-.11.04m.54.2s.01-.04.04-.06m-.01.11l-.02-.05m.77.99c-.07-.1-.49-.64-.75-.94m.7.98s.06-.01.05-.04m-.61.04h.56m-.62-.04s.03.04.06.04m-.61-.83c.18.23.43.6.54.79m-6.2-1.68c0-.13.05-.18.18-.18m-4.49-.32l-.02-.04m-22.82,3.56l1.26-1.86m-2.53,0h0m-.29-.89c0,.33.11.64.29.89m1.26-2.44c-.85,0-1.55.69-1.55,1.55m3.09,0c0-.85-.69-1.55-1.55-1.55m1.26,2.44c.18-.25.28-.56.28-.89m-1.55-2.34c1.62,0,2.94,1.32,2.94,2.94m17.89,1.72s.04-.02.04-.04m7.19-2.91c-.36.32-.38,1.58,0,1.9m-24.19-.66c0-2.14-1.73-3.87-3.87-3.87m27.49,2.49s-.02-.05-.04-.05m.05.33v-.28m-.04.32s.04,0,.04-.04m-.35,2.73v-2.51m.18-.18h.13m-.82,2.74h.47m3.13-1.99c0,.73.24,1.19.94,1.19m-7.04-2.26v2.79m9.28-2.8c-.14.25-.44.72-.59.95m-12.49,1.65s-.04.02-.04.04m4.92-2.68h-1.04m1.89.77c0-.58-.43-.77-.85-.77m.42,1.5s0-.03.02-.04m-12.23.71v-2.12m-13.94,4.27c-1.38-.25-2.42-1.45-2.42-2.89m23.8,1.26s-.02-.04-.04-.04m-22.09-.92l1.26,1.86m20.81-3.57h-.47m.51.04s-.02-.04-.04-.04m0,2.26s.04-.02.04-.04m-.51.04h.47m10.36.04c.2,0,.37-.02.46-.06m-5.04.82s.02.05.04.05m-.04-2.64v2.59m-29.53-1.64c0-1.62,1.32-2.94,2.94-2.94m20.87,4.62v-.42m3.31-2.68s-.04.02-.04.04m-4.25,1.79h-.35m-18.84,3.39c1.81-.32,3.18-1.9,3.18-3.8m13.54.5c.32,0,.4-.17.4-.49m0,0c0-.12-.01-.12-.05-.12m0,0h-.78m0,0l-.02.03m0,0c0,.33.07.59.45.59m7.59.55c.35,0,.45-.11.45-.38m-.43-.45h-.27m-.01.83h.27m-.3-.8v.77m.04-.8s-.04,0-.04.04m.74.42c0-.35-.15-.44-.43-.45m-.31.8s0,.03.03.03m-14.28-2.11s0-.02-.03-.03m0,0c-.06-.01-.17-.02-.27-.02m0,.69h.28m0,0l.02-.02m0,0v-.62m-.71.28c0,.3.1.36.4.36m0-.69c-.33,0-.41.04-.41.34m2.34.55c0,.59.11.7.5.7m.3-.07v-1.25m0,0s0-.03-.03-.04m0,0s-.13-.02-.25-.02m0,0c-.45,0-.52.11-.52.68m.77.67l.02-.04m-.3.07c.12,0,.21-.01.27-.04m-11.92-.31c.16-.2.25-.45.25-.72m0,0c0-.65-.53-1.17-1.17-1.17m.35,1.94c.22.21.4.19.57-.05m-2.09-.72c.79,0,1.14.43,1.52.77m-.35-1.94c-.65,0-1.17.53-1.17,1.17m25.9.46h.26m-.3-.84v.8m0,0s.01.04.04.04m.75-.43c0-.35-.12-.43-.53-.43m0,0h-.24m.26.87c.29,0,.5-.08.5-.44m-.77-.43s-.03,0-.03.03m4.4.03c-.05-.12-.57-.12-.63,0m0,0c-.17.17-.18,1.15,0,1.33m.63,0c.17-.17.16-1.16,0-1.33m-.63,1.33c.05.12.58.12.63,0m0,0c.17-.17.16-1.16,0-1.33m-.63,0c-.17.17-.18,1.15,0,1.33m1.04.28c.36-.33.35-1.57,0-1.9m-1.45,0c-.36.32-.38,1.58,0,1.9" - }, - "children": [] - } - ] - }, - { - "name": "g", - "type": "element", - "value": "", - "attributes": { - "id": "Well_Labels" - }, - "children": [ - { - "name": "path", - "type": "element", - "value": "", - "attributes": { - "class": "cls-1", - "d": "m82.38,87.7c.27-.2.27-.71,0-.9m0,0c-.13-.11-.31-.16-.53-.16m0,0c-.22,0-.39.06-.53.16m0,0c-.28.2-.27.7,0,.9m0,0c.26.23.8.22,1.06,0m8.84.01c.12-.08.21-.19.28-.33m0,0c.07-.14.1-.3.1-.49m0,0c0-.12-.04-.23-.11-.33m0,0c-.07-.1-.17-.18-.29-.24m0,0c-.12-.06-.24-.09-.37-.09m0,0c-.67-.04-.9.7-.65,1.21m0,0c.06.12.15.22.26.29m0,0c.23.14.58.14.8-.02m10.22-.23c.26-.39.27-1.83,0-2.22m0,0c-.13-.23-.32-.34-.58-.34m0,0c-.25,0-.45.11-.59.34m0,0c-.27.38-.27,1.83,0,2.21m0,0c.24.46.92.46,1.16,0m-82.52.68h.33m-.41-.24c.05.06.08.14.08.24m-.34-.38c.12.03.2.08.25.14m-.06-.49h-.69m.69-2.84v2.84m.47-2.84h-.47m-6.54-43.11v-.41m-.23,1.6h-1.61m1.61.4v-.4m-1.61,1.57v-1.16m-.46,1.57h2.25m-2.25-3.57v3.57m2.11-10.97h-1.57m0,1.58v-1.18m1.75,1.18h-1.75m0-3.17h-.47m19.08,52.92v-.4m-1.79.83c-.07-.1-.1-.22-.1-.35m.37.62c-.11-.08-.2-.17-.27-.26m1.66,1.3c-.07-.13-.16-.25-.27-.34m.37.81c0-.18-.04-.34-.1-.47m-.04,1.01c.1-.15.14-.33.14-.53m-2.3.45c.1.17.24.31.43.41m-.13-.97h-.45m1.77.56c-.13.11-.3.17-.52.17m.71-.62c0,.18-.06.33-.19.44m-.96-1.53c.13.08.27.16.4.23m-.77-.5c.11.1.24.19.37.27m-.64-.62c.07.13.16.25.27.35m2.02-1.25h-2.4M10.03,15.72v-1.65m1.93,1.65h-1.93m1.93,1.93h.46m-.46-1.52v1.52m-1.93-1.52h1.93m-2.38,1.52h.46m-.46-3.57v3.57m.46-3.57h-.46m2.82,9h-.35m.35,1.84v-1.84m-1.34,1.85h1.34m-.56-.93c.04.1.06.19.07.28m-.24-.55c.07.08.13.17.17.27m-1.25-.47c.13-.05.26-.08.41-.08m-.75.33c.1-.11.21-.2.34-.25m-.56.69c.05-.18.13-.33.22-.44m-.17,1.9c-.09-.21-.13-.48-.13-.81m1.04,1.43c-.21,0-.39-.05-.54-.15m.95.05c-.12.07-.26.1-.41.1m.71-.37c-.08.11-.18.2-.3.27m.53.16c.12-.11.22-.23.29-.37m-.71.62c.16-.06.3-.15.42-.26m-.93.35c.18,0,.35-.03.51-.1m-1.32-.12c.23.14.5.22.82.22m-1.35-.85c.12.28.3.49.53.63m-.72-1.63c0,.39.06.72.19.99m0-1.98c-.12.27-.18.6-.18.98m1.52-1.82c-.31,0-.59.07-.81.22m2.08,27.64c-.14-.27-.35-.47-.63-.61m.83,1.61c0-.4-.07-.73-.2-.99m-1.9,2.76h.25m-.51,0h.26m-.58-3.57v3.57m.78-3.57h-.78m2.29,9.31c-.11-.11-.24-.2-.41-.26m.79,1.08c-.02-.15-.06-.3-.12-.44m-.37.44h.49m-.64-.38c.07.12.13.25.15.38m-.44-.67c.12.07.22.16.29.28m-.73-.39c.17,0,.31.04.43.11m-.98,2.62c-.15-.1-.26-.26-.34-.47m.88.62c-.22,0-.4-.05-.55-.15m.98.05c-.12.07-.26.1-.43.1m.73-.37c-.07.11-.17.2-.29.27m.44-.65c-.03.14-.08.27-.15.38m.63-.38h-.49m.28.58c.12-.17.19-.37.21-.58m-1.35,1.15c.25,0,.48-.05.67-.16m-1.49-.06c.23.14.5.22.82.22m-1.34-.84c.12.27.29.48.52.62m-.69-1.61c0,.39.06.72.18.99m0-1.98c-.12.28-.18.61-.18.99m2.77,8.45c.02-.09.04-.19.04-.29m-.16.53c.06-.07.1-.16.13-.25m-.36.44c.1-.06.17-.12.23-.2m-.26,1.84c.22-.16.34-.39.34-.69m-1.32.93c.44,0,.77-.08.99-.24m-2.13.24h1.15m-1.15-3.57v3.57m1.19-3.57h-1.19m.46,9.97l-.35-.97m1.82.97h-1.46m1.81-.97l-.35.97m.81-.97h-.47m-.83,3.57l1.29-3.57m-1.81,3.57h.52m-1.81-3.57l1.29,3.57m-.83-3.57h-.47m28.63,8.07c-.07-.13-.17-.24-.29-.33m.39.77c0-.16-.04-.31-.1-.44m0,.85c.06-.12.1-.26.1-.41m-.37.71c.12-.08.21-.18.27-.3m-.68.49c.16-.04.29-.11.41-.19m.11,1.5c.11-.14.16-.3.16-.5m-.6.81c.19-.07.34-.18.44-.31m-1.1.42c.24,0,.46-.04.65-.11m-1.27-.02c.19.08.39.13.62.13m-1.23-1.09c0,.23.06.43.17.59m.29-.59h-.46m.58.4c-.07-.1-.12-.24-.12-.4m.41.63c-.12-.05-.22-.13-.29-.23m.66.31c-.13,0-.25-.02-.37-.07m.79,0c-.11.05-.25.08-.42.08m.68-.28c-.06.08-.15.16-.26.2m.35-.48c0,.1-.03.19-.09.28m0-.59c.07.09.11.19.11.31m-.4-.53c.12.06.22.13.29.23m-1.02-.32h.31m-.31-.39v.39m.32-.4h-.32m.78-.08c-.13.05-.29.08-.46.08m.9-.67c0,.14-.04.26-.12.36m0-.71c.07.1.11.21.11.34m-.41-.57c.13.06.23.13.3.23m-.76-.31c.17,0,.32.02.46.08m-1.01.09c.14-.12.33-.18.55-.18m-.82.69c.04-.22.12-.39.27-.51m-.73.51h.46m-.29-.57c-.11.16-.17.35-.17.57m.61-.95c-.19.09-.34.21-.44.37m1.09-.51c-.25,0-.46.05-.65.13m1.2-.07c-.17-.05-.35-.07-.55-.07m9.86,3.63v-2.26m-.47,2.26h.47m-2.13-2.24l1.67,2.24m9.84-3.05c-.11-.18-.26-.32-.45-.43m-1.04,2.16c.14.05.29.07.46.07m-.85-.31c.12.11.25.19.39.24m-.3.92l-.09-1.15m.23-.21c-.08-.05-.17-.13-.26-.23m.51.32c-.08-.02-.17-.05-.25-.1m-.48-.79c.06-.13.13-.25.22-.36m1.46-.57c-.19-.1-.42-.15-.68-.15m10.1.59c-.1-.18-.24-.33-.43-.43m-1.45,2.44c-.07-.23-.09-.51-.07-.85m.37,1.37c-.14-.12-.24-.29-.31-.52m1.37.97c.17-.07.31-.18.42-.32m-.96.43c.2,0,.38-.04.55-.11m8.36-2.97c-.06-.2-.1-.38-.11-.53m.34,1.19c-.09-.23-.16-.45-.22-.66m1.25,3.04v-.4m-2.51.4h2.51m-2.51-.4v.4m2.01-.4h-2.01m11.3-1.02c-.1-.13-.24-.23-.4-.3m-1.68,1.32c.35.48,1.14.54,1.66.33m-1.66-1.35c-.22.26-.2.76,0,1.02m.41-1.32c-.16.07-.3.17-.4.3m.06-.47c.11.09.23.14.35.17m-.55-1.43c-.19.26-.23.65-.06.94m.54-1.29c-.2.08-.36.2-.48.36m1.19-.48c-.28,0-.51.04-.72.13m10.86.74c-.11-.28-.27-.5-.48-.65m-.71,3.45c.28,0,.52-.07.72-.22m-1.37.06c.18.1.4.16.66.16m.37-2.29c-.13-.05-.26-.08-.42-.08m7.97,2.32v-3.57m-.32,3.57h.32m-.41-.24c.05.06.08.14.08.24m-.15-.73h-.69m.69-2.84v2.84m.47-2.84h-.47m1.67.4c-.4.47-.44,1.77-.18,2.39m1.11-2.85c-.39,0-.7.16-.93.46m1.86,0c-.23-.31-.54-.46-.93-.46m.93,3.21c.46-.55.46-2.19,0-2.74m6.04,2.92c.05.06.08.14.08.24m-.34-.38c.12.03.2.08.25.14m10.37-2.85v-.07m.1.43c-.07-.1-.1-.22-.1-.35m.37.62c-.11-.08-.2-.17-.27-.26m1.66,1.3c-.07-.13-.16-.25-.27-.34m.37.81c0-.18-.04-.34-.1-.47m-.04,1.01c.1-.15.14-.33.14-.53m-.55.89c.18-.08.31-.2.41-.36m-1.03.48c.24,0,.44-.04.62-.13m-1.32-.02c.19.1.42.15.69.15m-1.12-.56c.1.17.24.31.43.41m-.58-.97c0,.21.05.4.15.57m1.1.16c-.24,0-.42-.07-.56-.2m1.08.03c-.13.11-.3.17-.52.17m.71-.62c0,.18-.06.33-.19.44m-.55-1.3c.13.07.26.15.37.23m-.77-.46c.13.08.27.16.4.23m-.77-.5c.11.1.24.19.37.27m-.64-.62c.07.13.16.25.27.35m-.37-1.25v.42m2.39-.42h-2.39m-.92,3.57v-3.57m-.33,3.57h.33m-.41-.24c.05.06.08.14.08.24m-.34-.38c.12.03.2.08.25.14m-.75-.19c.21,0,.38.01.5.05m-.5-.35v.3m.69-.3h-.69m.69-2.84v2.84m.47-2.84h-.47m-6.05,3.57v-3.57m-.32,3.57h.32m-.41-.24c.05.06.08.14.08.24m-.34-.38c.12.03.2.08.25.14m-.75-.49v.3m.69-.3h-.69m.69-2.84v2.84m.47-2.84h-.47m-82.58,3.47c.19.1.42.15.69.15m79.19-.48c.21,0,.38.01.5.05M12.14,70.23c-.1-.13-.27-.24-.52-.33m.66.82c0-.19-.05-.35-.15-.49m44.07,15.54c0-.25-.06-.46-.16-.65M10.48,33.67v-1.59m1.84,27.69c-.06-.14-.15-.27-.26-.38m88.14,28.62c.48.35,1.25.25,1.6-.26M10.44,26.14c-.16-.1-.28-.25-.36-.47m45.9,62.1h-1.73M10.32,59.78c.17-.23.42-.35.74-.35m-1,1.44c0-.5.08-.86.25-1.09m-.14,1.91c-.08-.21-.12-.48-.12-.81m88,26.91c.12.03.2.08.25.14m-.75-.19c.21,0,.38.01.5.05m-.5-.35v.3M10.4,43.08h1.61m-.25,19.46c.19-.11.35-.25.47-.42m96.87,25.62c.21,0,.38.01.5.05m-55.46-2.52c.09-.11.2-.19.32-.25M10.27,59.25c-.23.14-.4.36-.52.63m1.34-.85c-.32,0-.6.07-.82.22m1.38-.13c-.16-.06-.35-.1-.56-.1m81.08,27.41c0-.4-.05-.74-.16-1.02m0,1.98c.11-.26.17-.59.17-.96m-.63,1.57c.2-.14.36-.35.47-.61m-73.91.05v.3m71.64-.11c.1.18.24.33.42.43m.62-2.21c-.23,0-.44.05-.62.14M11.61,50.28c-.28-.14-.63-.2-1.07-.2m80.96,36.08c-.09-.1-.2-.17-.32-.22m27.76-.02c-.13-.07-.26-.14-.37-.22m.76.44c-.13-.08-.26-.15-.4-.22m.76.48c-.11-.1-.23-.18-.36-.26M10.59,53.64c.44,0,.79-.08,1.07-.22m78.61,31.65c.13-.1.29-.15.47-.15M9.86,53.65h.22m79.7,31.33c-.12.14-.18.3-.21.5M9.76,53.65h.11m80.89,30.9c-.2,0-.38.04-.55.11M11.82,23.33c-.1-.09-.21-.16-.35-.22m26.45,63.71c-.11-.14-.29-.25-.52-.34m80.68.61h-.46m.69.53c-.14-.13-.22-.31-.24-.53M12.37,69.04c0-.32-.12-.56-.35-.72m107.46,18.53c.07.1.1.22.1.36m-.37-.63c.11.08.2.17.27.27M11.91,24.59h-.87m.85-.31v.31m-.52-1.07c.11.05.2.12.28.2m106.03,61.3c0,.18.04.34.1.47M10.99,23.44c.15,0,.28.02.39.08m71.11,62.94c.26-.05.49-.26.62-.49M9.95,24.87c0-.25.02-.47.08-.65m72.87,63.56c.2-.26.22-.76,0-1.02M10.48,34.07h1.57m68.55,51.89c.07.13.16.24.27.32M11.85,25.55c-.02.13-.07.25-.15.37m.64-.37h-.49m.37.43c.07-.14.11-.28.12-.43m70.24,59.12c-.2-.08-.44-.13-.72-.13m1.2.48c-.11-.15-.27-.27-.48-.36m-9.83-.07h-.5M10.4,42.67v-1.19m0,0h1.84m-2.08-18.25c-.23.14-.4.35-.52.62m2.55,20.78v-.4m42.68,40.68c.25,0,.46.07.61.22M11.97,14.08v1.65m.46-1.65h-.46m.46,3.57v-3.57m59.83,70.52c.02.16.07.36.14.6M10.03,17.65v-1.52m16.66,68.9c0,.18.04.34.1.47m-.18,1.59c0,.21.05.4.15.57m38.39-1.89c0-.24-.05-.45-.15-.63m-1.04,2.71c-.21,0-.38-.06-.53-.18m-8.53-1.12c-.1,0-.19,0-.27-.02m10.28,1.28c.11-.14.18-.3.2-.5m-37.25.53c-.24,0-.42-.07-.56-.2m80.6.55h.33M12.05,23.08v.54m51.15,64.36c.21.16.46.23.76.23m-37.26-3.62v.42m1.2,3.2c.24,0,.44-.04.62-.13m8.26-1.43c.16,0,.3.04.42.09m62.56.64c.11.28.25.48.44.62m-9.99-3.35c-.17.08-.31.18-.43.32M11.47,23.12c-.14-.06-.3-.08-.49-.08m43.48,61.99c.12-.06.26-.09.42-.09m-1.36.53l.4.17m-35.82,2.11c.21,0,.38.01.5.05m9.93.31c.18-.08.31-.2.41-.36m18.34-2.23h-.59m73.42-.51v-.4m-1.88.4h1.88m-75.55.51v.42M11.65,53.42c.27-.14.47-.34.6-.59m25.54,31.98c-.12-.09-.27-.16-.44-.2M12.05,23.62c-.06-.1-.14-.2-.24-.29m26.27,63.96c0-.18-.06-.34-.17-.48m-10.6.8c-.14-.13-.22-.31-.24-.53m8.69.63c.11.16.26.29.45.37m36.86-1.21c.16.3.34.6.54.9m-36.05-1.82c-.08.1-.19.17-.32.23m17.76.75c.22,0,.41-.04.6-.13m26.87,1.31c.18-.08.32-.19.43-.33M12.05,34.07v-.4m60.62,52.32c.11.29.24.58.4.89m-18.96-.69h-.02m53,1.25v.3m-51.36-1.99c0,.16-.04.3-.11.41m18.51,1.61c-.13-.17-.25-.37-.38-.59m-18.25-2.04c.15.14.23.35.23.6m.27,2.42v-.4m-44.36-17.88c.14-.03.26-.07.35-.13m60.42,15.43c.07.24.16.5.27.79M12.23,41.08h-2.3m43.92,47.09h2.13m34.16-2.18c-.18.09-.32.22-.42.4m-62.53-1.31v-.07m19.47.91h.59m-20.06-.91h1.89m62.61,1.49c-.04-.12-.11-.23-.19-.33m-28.24.45c.09.09.2.16.32.21m1.54.47h-.43M12.26,52.84c.13-.26.19-.58.19-.95m51.47,32.66c-.28,0-.52.07-.72.22m-7.6,2.04c.18-.09.33-.22.44-.39M11.05,24.59v.34m96.72,59.68v2.84m.47-2.84h-.47m-61.55,0v.91m18.36-.81c-.19-.1-.41-.16-.66-.16m19.2,1.42c.17-.29.13-.68-.06-.94m-27.45,1.14c-.07.12-.17.21-.29.28M12.23,35.65v-.4m41.85,50.94l-.38.12m19.68.19c-.11-.24-.21-.48-.3-.71m-19.38.52l.16,1.86m9.73-1.35c.12.05.25.07.4.07m-9.86-.71h-.02m35.46-.71h.44m-25.57,2.22c-.13.1-.28.15-.47.15m1.05-1.47c.1-.17.15-.37.15-.6m-45.91,2.41v-3.57m71.49.32c.21,0,.39.06.54.17m-28.08-.33c-.2.14-.36.35-.47.61m11.01,1.81c-.12-.22-.24-.45-.35-.69M12.19,44.25h-1.79m81.12,40.52c-.21-.15-.47-.22-.77-.22m-34.72,1.86c.11-.17.17-.38.17-.64m6.52,1.56c.11.29.26.5.47.66m28.08-2.9c.15.11.26.28.34.51m-28.89-.23c-.11.27-.17.59-.17.96m-16.34-.82h-1.68m10.37-.97c-.22,0-.43.04-.61.12m1.02,1.78c-.12.07-.26.1-.41.1m-7.65-.63v-.4M10.01,32.08v3.57m43.81,49.34c-.13.14-.23.3-.3.48m36.49.01c.04-.16.13-.29.26-.4m-.54,1.31c-.1.17-.15.37-.15.61m-42.91-1.49v-.91m-34.64-16.28c-.23-.16-.58-.24-1.03-.24m17.7,18.31c-.11-.1-.23-.18-.36-.26m34.73.16c.05.12.11.23.2.32m-.71-.28c0,.38.05.72.16,1m1.26-.43c.24,0,.44-.05.62-.14m-36.76-.42c.13.07.26.15.37.23m80.03,1.6v-3.57m-.47,2.84h-.69m-78.75-1.31c-.13-.08-.26-.15-.4-.22m61.64,1.09c0,.24.05.45.15.63m-25.13-.87c.18-.09.31-.22.41-.39m-36.8.2c.11.08.2.17.27.27m25.81-2.18c-.19.08-.35.19-.48.32m-7.14-.38h-.45M10.01,35.65h2.22m79.39,49.95c.07.23.1.53.08.89m-27,.8c-.05.17-.13.3-.26.4m-35.95-.85c.07.1.1.22.1.36m-.65-1.29c-.13-.07-.26-.14-.37-.22m55.05.23c.18-.25.13-.64-.13-.83m-.17,1.06c.13-.06.23-.14.3-.24m-.13-.82c-.16-.12-.37-.18-.63-.18m0,0c-.59-.05-1.1.48-.76,1.01m0,0c.24.36.85.4,1.22.24M11.05,69.69c.16,0,.3-.02.43-.06m.43-.57c0-.2-.08-.35-.23-.45m0,0c-.16-.1-.38-.15-.68-.15m-.72,0v1.23m0,0h.78m-.05-1.23h-.72m1.52.96c.08-.09.12-.22.12-.36m-.43.57c.13-.04.23-.11.31-.2m-.19,1.7c.16-.1.24-.25.24-.46m0,0c0-.11-.02-.2-.07-.28m-.72-.3h-.78m1.31.12c-.08-.05-.16-.08-.25-.1m-.34,1.16c.25,0,.46-.05.62-.15m-.28-1.01c-.09-.02-.18-.03-.28-.03m-.78,0v1.19m1.5-.89c-.05-.08-.11-.13-.19-.18m-1.31,1.07h.71m-.6,7.2l.61,1.73m0,0l.61-1.73m0,0h-1.22m35.82,7.44h-1.23m1.23,1.66v-1.66m-1.23,0l1.23,1.66m18.37-1.41c.12-.23.12-.62-.01-.85m0,0c-.06-.12-.16-.22-.27-.29m-.8.01c-.12.08-.21.19-.28.33m1.1,1.06c.11-.06.2-.15.26-.27m-.67-1.24c-.16,0-.29.04-.41.12m.8-.02c-.12-.07-.24-.1-.39-.1m-.68.45c-.07.14-.1.3-.1.48m.4.58c.24.13.58.12.79,0m-1.2-.58c0,.25.19.46.41.58M11.8,52.7c.11-.2.17-.47.17-.82m-1.4,1.38c.29,0,.55-.04.75-.13m-.78-2.66h-.31m1.08.14c-.22-.09-.47-.14-.76-.14m1.25.58c-.12-.2-.28-.35-.49-.44m.66,1.27c0-.35-.06-.62-.17-.83m-.47,2.07c.21-.08.37-.23.48-.43m-1.57.56h.34m-.34-2.79v2.79m71.1,33.55c-.28.2-.27.7,0,.9" - }, - "children": [] - } - ] - }, - { - "name": "g", - "type": "element", - "value": "", - "attributes": { - "id": "Wells" - }, - "children": [ - { - "name": "path", - "type": "element", - "value": "", - "attributes": { - "class": "cls-1", - "d": "m122.09,79.84l.1-.09m-1.63,2.61l.04-.13m-104.49,0l.04.13m-1.63-2.61l.1.09m0-64.98l-.1.1m1.63-2.61l-.04.13m104.49,0l-.04-.13m1.63,2.61l-.1-.1m-14.23,5.68v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m-3.69,3.31v.38m3.31,3.31c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-3.69-3.69c1.64-.38,2.93-1.67,3.31-3.31m-5.31,3.69v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m3.31,3.31c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m21.31-3.31v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m3.31,3.31c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-77.69-3.31v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m-3.69,3.31v.38m3.31,3.31c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-3.69-3.69c1.64-.38,2.93-1.67,3.31-3.31m9.38,61h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m0-7c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m3.31,3.31c-.38-1.64-1.67-2.93-3.31-3.31m7,0v-.38m0-8.62v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m3.69,3.31h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-.38-7h.38m-.38,7c-.38-1.64-1.67-2.93-3.31-3.31m43-18v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m.38,7h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-.38,0c-.38-1.64-1.67-2.93-3.31-3.31m0-.38v.38m16,18v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m0,7c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m0-7h.38m-3.69,3.31v.38m7,0c-1.64.38-2.93,1.67-3.31,3.31m0-16c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m7,0c-1.64.38-2.93,1.67-3.31,3.31m-.38-7h.38m3.31,3.69v-.38m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m3.69-12.31v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m3.31,3.31c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m21.31,14.69v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m0,7c-.38-1.64-1.67-2.93-3.31-3.31m3.31-3.69h.38m-3.69,3.31v.38m3.69,3.31h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-23.69-48.31v-.38m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-3.69-3.69c1.64-.38,2.93-1.67,3.31-3.31m.38,0c.38,1.64,1.67,2.93,3.31,3.31m-7,0v.38m3.31-3.69h.38m30.31,12.69v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m.38,7h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-.38-7h.38m-3.69,3.31v.38m3.31,3.31c-.38-1.64-1.67-2.93-3.31-3.31m12.69,48.31h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m3.31-3.31v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m0,0h.38m-.38,7c-.38-1.64-1.67-2.93-3.31-3.31m0-.38v.38m7-9v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m.38,7h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-.38,0c-.38-1.64-1.67-2.93-3.31-3.31m3.31-3.69h.38m-3.69,3.31v.38M62.87,20.55v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-.38-7h.38m-3.69,3.31v.38m0-.38c1.64-.38,2.93-1.67,3.31-3.31m5.69,3.31v.38m3.69,3.31h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m0-7c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m3.69,3.69v-.38m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m3.31-3.69h.38m21.31,3.69v-.38m-3.69-3.31h.38m0,7h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-.38,0c-.38-1.64-1.67-2.93-3.31-3.31m0-.38v.38m3.69-3.69c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31M23.55,61.85c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m-3.69,3.31v.38m7,0c-1.64.38-2.93,1.67-3.31,3.31m0,0h-.38m3.69-3.31v-.38m-7,0c1.64-.38,2.93-1.67,3.31-3.31m0,7c-.38-1.64-1.67-2.93-3.31-3.31m7-36v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-3.31,3.69h-.38m-3.31-3.69v.38m0-.38c1.64-.38,2.93-1.67,3.31-3.31m0,7c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m-.38-7h.38m-.38-9h.38m3.31,3.69c-1.64.38-2.93,1.67-3.31,3.31m3.31-3.31v-.38m-7,0v.38m3.69-3.69c.38,1.64,1.67,2.93,3.31,3.31m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m-3.31-3.69c1.64-.38,2.93-1.67,3.31-3.31m21.69,3.69v-.38m-3.69-3.31h.38m-3.69,3.31v.38m0-.38c1.64-.38,2.93-1.67,3.31-3.31m3.69,3.69c-1.64.38-2.93,1.67-3.31,3.31m-.38,0c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m.38-7c.38,1.64,1.67,2.93,3.31,3.31m5.31-3.31h.38m-3.69,3.31v.38m3.69,3.31h-.38m3.69-3.31v-.38m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m3.69-3.69c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m3.69,3.69c-1.64.38-2.93,1.67-3.31,3.31m30.31,32.69v-.38m-3.69-3.31h.38m-.38,7c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-3.69-3.69c1.64-.38,2.93-1.67,3.31-3.31m.38,0c.38,1.64,1.67,2.93,3.31,3.31m-7,0v.38m27,17.62v.38m3.69,3.31h-.38m.38-7c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m0,7c-.38-1.64-1.67-2.93-3.31-3.31m3.31-3.69h.38m3.31,3.69v-.38m0,.38c-1.64.38-2.93,1.67-3.31,3.31M50.55,25.87c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m0,7c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m3.69-3.31v-.38m0,.38c-1.64.38-2.93,1.67-3.31,3.31m-.38-7h.38m-3.69,3.31v.38m-14.69,41.31h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m.38,0c.38,1.64,1.67,2.93,3.31,3.31m-3.31,3.69h-.38m-3.31-3.69v.38m3.31,3.31c-.38-1.64-1.67-2.93-3.31-3.31m7,0v-.38m0,.38c-1.64.38-2.93,1.67-3.31,3.31m0-16c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m3.69,3.69v-.38m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m-3.31-3.69v.38m7,0c-1.64.38-2.93,1.67-3.31,3.31m0-16c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m0,0h.38m0,7h-.38m-3.31-3.69v.38m7,0v-.38m0,.38c-1.64.38-2.93,1.67-3.31,3.31m-.38,0c-.38-1.64-1.67-2.93-3.31-3.31m7-9v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m3.69,3.31h-.38m0,0c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m-3.69-21.69c1.64-.38,2.93-1.67,3.31-3.31m0,7c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m0,0h-.38m-3.31-3.69v.38m7,0v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m-12.69,48.31v.38m3.69,3.31h-.38m.38-7c.38,1.64,1.67,2.93,3.31,3.31m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m7,0v-.38m0,.38c-1.64.38-2.93,1.67-3.31,3.31m-.38-7h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m18.38-45c.38,1.64,1.67,2.93,3.31,3.31m-7,0v.38m3.31,3.31c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-.38-7h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m3.69,3.69v-.38m5.69,14.69c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m.38,7h-.38m-3.31-3.69v.38m7,0v-.38m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m-3.69-12.69c1.64-.38,2.93-1.67,3.31-3.31m0,7c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-3.69-3.69v.38m7,0v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m5.31,12.31c1.64-.38,2.93-1.67,3.31-3.31m.38,7h-.38m3.69-3.31v-.38m-3.69-3.31h.38m0,0c.38,1.64,1.67,2.93,3.31,3.31m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m-3.69-3.69v.38m3.69-12.69c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m-.38,7c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m-3.31-3.69c1.64-.38,2.93-1.67,3.31-3.31m3.69,3.69v-.38m0,.38c-1.64.38-2.93,1.67-3.31,3.31m-3.69-3.69v.38m9,17.62c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m7,0c-1.64.38-2.93,1.67-3.31,3.31m0,0h-.38m0-7h.38m0,0c.38,1.64,1.67,2.93,3.31,3.31m0,.38v-.38m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m3.69-12.69c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m3.69,3.31h-.38m0-7h.38m-.38,7c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m3.31-3.31v-.38m-7-9c1.64-.38,2.93-1.67,3.31-3.31m.38,7h-.38m-3.31-3.69v.38m3.31,3.31c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m3.31-3.31v-.38m-3.69-3.31h.38m0,0c.38,1.64,1.67,2.93,3.31,3.31m0-8.62v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m0,7h-.38m-3.31-3.69c1.64-.38,2.93-1.67,3.31-3.31m0,7c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m-3.69-3.69v.38m12.69,32.31c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m.38,7h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-3.69-3.69v.38m7,0v-.38m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m3.31-3.69h.38m18,9c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m.38,7h-.38m3.69-3.31v-.38m-7,0v.38m3.31,3.31c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m-.38-7h.38m0-9c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m.38,7h-.38m-3.31-3.69v.38m7,0c-1.64.38-2.93,1.67-3.31,3.31m3.31-3.31v-.38m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m0-9.38c1.64-.38,2.93-1.67,3.31-3.31m.38,7h-.38m-3.31-3.69v.38m3.31-3.69h.38m3.31,3.69c-1.64.38-2.93,1.67-3.31,3.31m-.38,0c-.38-1.64-1.67-2.93-3.31-3.31m3.69-3.69c.38,1.64,1.67,2.93,3.31,3.31m0,.38v-.38m-3.31-12.31c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m3.69,3.31h-.38m0-7h.38m3.31,3.69v-.38m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m0-25c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m.38,7h-.38m-3.31-3.69v.38m7,0v-.38m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m3.31-3.69h.38m3.31,3.69c-1.64.38-2.93,1.67-3.31,3.31m8.62,2h.38m-.38,7c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m-3.31-3.69v.38m7,0v-.38m-7,0c1.64-.38,2.93-1.67,3.31-3.31m.38,0c.38,1.64,1.67,2.93,3.31,3.31m0,.38c-1.64.38-2.93,1.67-3.31,3.31m8.62-16h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m0,7c-.38-1.64-1.67-2.93-3.31-3.31m3.69-3.69c.38,1.64,1.67,2.93,3.31,3.31m-3.31,3.69h-.38m-3.31-3.69v.38m7,0v-.38m0,.38c-1.64.38-2.93,1.67-3.31,3.31m-36,38c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m0,7c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m0,0h-.38m-3.31-3.69v.38m3.31-3.69h.38m3.31,3.69v-.38m2,0c1.64-.38,2.93-1.67,3.31-3.31m.38,0c.38,1.64,1.67,2.93,3.31,3.31m-3.31,3.69h-.38m0-7h.38m3.31,3.69v-.38m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m0-.38v.38m7,0c-1.64.38-2.93,1.67-3.31,3.31m0-52c.38,1.64,1.67,2.93,3.31,3.31m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m-3.31-3.69c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m7,0c-1.64.38-2.93,1.67-3.31,3.31m3.31-3.31v-.38m-3.69-3.31h.38m17.62,34c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m.38-7c.38,1.64,1.67,2.93,3.31,3.31m0,.38v-.38m-7,0c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m7,0c-1.64.38-2.93,1.67-3.31,3.31m-.38-7h.38m-3.69-5.69c1.64-.38,2.93-1.67,3.31-3.31m.38,7h-.38m0-7h.38m-3.69,3.31v.38m3.69-3.69c.38,1.64,1.67,2.93,3.31,3.31m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m3.31-3.31v-.38m-84.31-12.31c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m3.31,3.31c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m3.31-3.31v-.38m-3.31,3.69h-.38m0-7h.38m26.62,18h.38m-.38,7c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m0-7c.38,1.64,1.67,2.93,3.31,3.31m-7,0v.38m0-.38c1.64-.38,2.93-1.67,3.31-3.31m3.69,3.69v-.38m5.69-3.31c.38,1.64,1.67,2.93,3.31,3.31m-3.31,3.69h-.38m-3.31-3.69c1.64-.38,2.93-1.67,3.31-3.31m0,0h.38m-.38,7c-.38-1.64-1.67-2.93-3.31-3.31m0-.38v.38m7,0c-1.64.38-2.93,1.67-3.31,3.31m3.31-3.31v-.38m-7,9c1.64-.38,2.93-1.67,3.31-3.31m0,7c-.38-1.64-1.67-2.93-3.31-3.31m3.69,3.31h-.38m.38-7c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m3.31,3.69v-.38m-7,0v.38m7,0c-1.64.38-2.93,1.67-3.31,3.31m-27-34c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m.38,7h-.38m0,0c-.38-1.64-1.67-2.93-3.31-3.31m3.31-3.69h.38m-3.69,3.31v.38m7,0c-1.64.38-2.93,1.67-3.31,3.31m3.31-3.31v-.38m38-9c1.64-.38,2.93-1.67,3.31-3.31m.38,0c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m3.31,3.69v-.38m-7,0v.38m3.31,3.31c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m0,0h-.38m-57.31,14.31v.38m3.69-3.69c.38,1.64,1.67,2.93,3.31,3.31m-3.31,3.69h-.38m3.69-3.31v-.38m-7,0c1.64-.38,2.93-1.67,3.31-3.31m0,7c-.38-1.64-1.67-2.93-3.31-3.31m3.31-3.69h.38m3.31,3.69c-1.64.38-2.93,1.67-3.31,3.31m27,27h-.38m-3.31-3.69c1.64-.38,2.93-1.67,3.31-3.31m0,0h.38m-.38,7c-.38-1.64-1.67-2.93-3.31-3.31m3.69-3.69c.38,1.64,1.67,2.93,3.31,3.31m0,.38c-1.64.38-2.93,1.67-3.31,3.31m3.31-3.31v-.38m-7,0v.38m45-36.38c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m3.69,3.31h-.38m0-7h.38m3.31,3.69c-1.64.38-2.93,1.67-3.31,3.31m-.38,0c-.38-1.64-1.67-2.93-3.31-3.31m3.69-3.69c.38,1.64,1.67,2.93,3.31,3.31m0,.38v-.38m-39.31-5.31h-.38m.38-7c.38,1.64,1.67,2.93,3.31,3.31m0,.38c-1.64.38-2.93,1.67-3.31,3.31m-.38,0c-.38-1.64-1.67-2.93-3.31-3.31m3.31-3.69h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m7,0v-.38m-39.31,23.69c.38,1.64,1.67,2.93,3.31,3.31m-7,0v.38m3.69,3.31h-.38m3.69-3.31v-.38m-7,0c1.64-.38,2.93-1.67,3.31-3.31m3.69,3.69c-1.64.38-2.93,1.67-3.31,3.31m-.38-7h.38m-.38,7c-.38-1.64-1.67-2.93-3.31-3.31m27,8.62c1.64-.38,2.93-1.67,3.31-3.31m.38,0c.38,1.64,1.67,2.93,3.31,3.31m-3.31,3.69h-.38m0-7h.38m3.31,3.69v-.38m0,.38c-1.64.38-2.93,1.67-3.31,3.31m-.38,0c-.38-1.64-1.67-2.93-3.31-3.31m0-.38v.38m30.31-23.69c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m-3.69-3.69c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m3.69,3.31h-.38m.38-7c.38,1.64,1.67,2.93,3.31,3.31m-3.69-3.31h.38m3.31,3.69v-.38m-39.31,5.69c.38,1.64,1.67,2.93,3.31,3.31m-7,0c1.64-.38,2.93-1.67,3.31-3.31m3.69,3.69v-.38m-3.69-3.31h.38m-3.69,3.31v.38m7,0c-1.64.38-2.93,1.67-3.31,3.31m0,0h-.38m0,0c-.38-1.64-1.67-2.93-3.31-3.31m21.69,30.31h-.38m-3.31-3.69c1.64-.38,2.93-1.67,3.31-3.31m.38,0c.38,1.64,1.67,2.93,3.31,3.31m0,.38v-.38m-3.69-3.31h.38m-.38,7c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m-3.69-3.69v.38m-18-36.38c1.64-.38,2.93-1.67,3.31-3.31m0,7c-.38-1.64-1.67-2.93-3.31-3.31m3.31-3.69h.38m3.31,3.69c-1.64.38-2.93,1.67-3.31,3.31m0-7c.38,1.64,1.67,2.93,3.31,3.31m-3.31,3.69h-.38m-3.31-3.69v.38m7,0v-.38m23.69,23.69c.38,1.64,1.67,2.93,3.31,3.31m0,.38v-.38m-7,0c1.64-.38,2.93-1.67,3.31-3.31m0,0h.38m0,7h-.38m3.69-3.31c-1.64.38-2.93,1.67-3.31,3.31m-.38,0c-.38-1.64-1.67-2.93-3.31-3.31m0-.38v.38m-27-9.38c1.64-.38,2.93-1.67,3.31-3.31m3.69,3.69c-1.64.38-2.93,1.67-3.31,3.31m-3.69-3.69v.38m3.69-3.69c.38,1.64,1.67,2.93,3.31,3.31m-3.31,3.69h-.38m0-7h.38m3.31,3.69v-.38m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m30.69,14.31c.38,1.64,1.67,2.93,3.31,3.31m-3.31,3.69h-.38m-3.31-3.69v.38m3.31-3.69h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m3.69,3.69c-1.64.38-2.93,1.67-3.31,3.31m-.38,0c-.38-1.64-1.67-2.93-3.31-3.31m7,0v-.38m14.69-39.31c.38,1.64,1.67,2.93,3.31,3.31m0,.38v-.38m-3.69-3.31h.38m0,7h-.38m-3.31-3.69c1.64-.38,2.93-1.67,3.31-3.31m-3.31,3.31v.38m3.31,3.31c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m30.31-3.31v-.38m-3.31-3.31c.38,1.64,1.67,2.93,3.31,3.31m-3.31,3.69h-.38m-3.31-3.69v.38m3.31,3.31c-.38-1.64-1.67-2.93-3.31-3.31m3.31-3.69h.38m-3.69,3.31c1.64-.38,2.93-1.67,3.31-3.31m3.69,3.69c-1.64.38-2.93,1.67-3.31,3.31m-3.69,14.31v.38m3.69-3.69c.38,1.64,1.67,2.93,3.31,3.31m0,.38v-.38m-7,0c1.64-.38,2.93-1.67,3.31-3.31m.38,7h-.38m0,0c-.38-1.64-1.67-2.93-3.31-3.31m7,0c-1.64.38-2.93,1.67-3.31,3.31m-.38-7h.38m3.31-5.31v-.38m-3.69,3.69c-.38-1.64-1.67-2.93-3.31-3.31m0-.38c1.64-.38,2.93-1.67,3.31-3.31m.38,0c.38,1.64,1.67,2.93,3.31,3.31m0,.38c-1.64.38-2.93,1.67-3.31,3.31m-3.69-3.69v.38m3.69,3.31h-.38m0-7h.38m8.65-28.24l.08.06m-2.32-3.72l.02.1M14.52,79.1l-.08-.06m2.32,3.72l-.02-.1m0-70.59l.02-.1m-2.32,3.72l.08-.06m105.46,67.04l-.02.1m2.32-3.72l-.08.06m-78.79-27.24c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm14.75-9c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.33,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm7.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm34.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-75.25-9c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm1.9,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.3,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm16.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm25.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-19.45-9c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm34.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-82.15,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm70.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-28.45,9c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-46.45,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm7.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm79.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-21.25,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm1.9,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.3,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm25.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-84.25-9c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.33,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm43.85,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-12.25,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.33,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0ZM14.61,42.85c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm1.9,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.3,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm5.75,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.33,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm7.85,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm59.75,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm1.9,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.3,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-1.45-9c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0ZM25.71,24.87c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm25.85,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-10.45,9c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm43.85,9c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm25.85,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0ZM43.71,24.87c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm23.75,9c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.33,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm43.55-9c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0ZM14.61,15.87c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.33,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm7.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm16.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm61.55,63c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0ZM50.6,15.87c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.33,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm16.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm16.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm25.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-55.15,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm16.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm16.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm7.55,9c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-1.15-9c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-73.45,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm16.55,27c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-10.45,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm34.85,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-55.15,9c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm88.85,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-10.45,9c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-19.15-9c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-10.15,9c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-46.15-9c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm34.55,9c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-19.15,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-19.15,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-19.15-9c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm70.85,9c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-46.45,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm61.85-9c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-82.15,9c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-10.15,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-3.25,18c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm1.9,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.3,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm43.85-18c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm52.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-19.45,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-37.15-9c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-28.15,27c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm79.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-37.15-9c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm23.74,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.33,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-57.25,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm1.9,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.3,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm16.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-10.15,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm50.75,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm1.9,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.3,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-66.25-18c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm1.9,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.3,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-10.45,27c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm16.85,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-10.15,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm16.85,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-37.15-9c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm43.85,9c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-10.15-27c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm7.85,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm16.85,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-57.25,18c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm.33,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm43.55,9c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm7.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-82.45-9c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm79.55,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-10.15,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-.3,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-64.45,0c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm-.1,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.2,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm.03,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-1.45,9c-.05-3.18,4.94-3.18,4.9,0,.05,3.18-4.94,3.18-4.9,0Zm-1.8,0c-.08-5.52,8.58-5.52,8.5,0,.08,5.51-8.58,5.51-8.5,0Zm2.1,0c-.04-2.79,4.34-2.79,4.3,0,.04,2.79-4.34,2.79-4.3,0Zm-2.2,0c-.08-5.64,8.78-5.64,8.7,0,.08,5.64-8.78,5.64-8.7,0Zm2.23,0c-.04-2.75,4.28-2.75,4.24,0,.04,2.75-4.28,2.75-4.24,0Zm1.09,0c-.02-1.34,2.08-1.34,2.06,0,.02,1.34-2.08,1.34-2.06,0Zm.03,0c-.02-1.3,2.02-1.3,2,0,.02,1.3-2.02,1.3-2,0Zm-9.93-12.99c-3.28,1.48-3.3,6.49,0,7.97m0-34.97c-3.28,1.48-3.3,6.49,0,7.97m0-16.97c-3.28,1.48-3.3,6.49,0,7.97m0-16.97c-3.28,1.48-3.3,6.49,0,7.97m14.92-14.92h1.03m7.97,0h1.03m7.97,0c-1.48-3.28-6.49-3.3-7.97,0m7.97,0h1.03m7.97,0c-1.48-3.28-6.49-3.3-7.97,0m34.97,0h1.03m7.97,0c-1.48-3.28-6.49-3.3-7.97,0m7.97,0h1.03m7.97,0c-1.48-3.28-6.49-3.3-7.97,0m7.97,0h1.03m14.92,5.92v1.02m0,7.97c3.28-1.48,3.3-6.49,0-7.97m0,16.98c3.28-1.48,3.3-6.49,0-7.97m0,16.97c3.28-1.48,3.3-6.49,0-7.97m0,7.97v1.03m0,7.97c3.28-1.48,3.3-6.49,0-7.97m0,7.98v1.02m0,7.97c3.28-1.48,3.3-6.49,0-7.97m0,7.98v1.02m0,7.97c3.28-1.48,3.3-6.49,0-7.97m-14.92,14.92h-1.03m-25.97,0c1.48,3.28,6.49,3.3,7.97,0m-16.97,0c1.48,3.28,6.49,3.3,7.97,0m-7.98,0h-1.02m-7.98,0h-1.02m-7.98,0h-1.03m-16.97,0c1.48,3.28,6.49,3.3,7.97,0m-16.97,0c1.48,3.28,6.49,3.3,7.97,0m-14.92-5.92c-6.95,3.72,2.2,12.87,5.92,5.92m-5.92-32.92v-1.03m96.94,33.94h-1.02m-25.97,0c1.48,3.28,6.49,3.3,7.97,0m24.94-42.94v1.03m0,34.97v1.03M76.84,13.94h1.02m-60.94,51.94v-1.02m0-7.98v-1.02m0-34.98v-1.02m50.92-5.92c-1.48-3.28-6.49-3.3-7.97,0m-19.02,0c-1.48-3.28-6.49-3.3-7.97,0m27,66.86c1.48,3.28,6.49,3.3,7.97,0m46.03,0c3.72,6.95,12.87-2.2,5.92-5.92m-41.92,5.92h-1.02m10.03,0h-1.03m-9-66.86c-1.48-3.28-6.49-3.3-7.97,0m-51.94,24.94v-1.03m33.95,42.94c1.48,3.28,6.49,3.3,7.97,0m60.94-51.94v1.03m-60.94-15.94h1.02m7.98,0h1.02M16.93,47.88c-3.28,1.48-3.3,6.49,0,7.97M112.84,13.94c-1.48-3.28-6.49-3.3-7.97,0M16.93,74.88v-1.03m15.95,6.94h-1.03m-7.98,0h-1.02m0-66.86c-3.72-6.95-12.87,2.2-5.92,5.92m87.95,60.94c1.48,3.28,6.49,3.3,7.97,0M22.85,13.94h1.02m-6.94,42.94c-3.28,1.48-3.3,6.49,0,7.97M112.84,13.94h1.02m-18,66.86c1.48,3.28,6.49,3.3,7.97,0M85.84,13.94c-1.48-3.28-6.49-3.3-7.97,0m-36,66.86h-1.03m1.03,0c1.48,3.28,6.49,3.3,7.97,0M119.78,19.85c6.95-3.72-2.2-12.87-5.92-5.92m-18,66.86h-1.03M16.93,29.88v-1.03m14.92-14.92c-1.48-3.28-6.49-3.3-7.97,0" - }, - "children": [] - } - ] - } - ] - } -} diff --git a/shared-data/module/schemas/3.json b/shared-data/module/schemas/3.json index 21168807c05..09504bbe494 100644 --- a/shared-data/module/schemas/3.json +++ b/shared-data/module/schemas/3.json @@ -84,13 +84,13 @@ "heaterShakerModuleType", "magneticBlockType", "absorbanceReaderType", - "flexStackerType" + "flexStackerModuleType" ], "type": "string" }, "model": { "type": "string", - "pattern": "^(temperatureModule|magneticModule|thermocyclerModule|heaterShakerModule|magneticBlock|absorbanceReader|flexStacker)V[0-9]+$" + "pattern": "^(temperatureModule|magneticModule|thermocyclerModule|heaterShakerModule|magneticBlock|absorbanceReader|flexStackerModule)V[0-9]+$" }, "labwareOffset": { "$ref": "#/definitions/coordinates" }, "dimensions": { diff --git a/shared-data/python/opentrons_shared_data/module/types.py b/shared-data/python/opentrons_shared_data/module/types.py index 87b49613167..64907930396 100644 --- a/shared-data/python/opentrons_shared_data/module/types.py +++ b/shared-data/python/opentrons_shared_data/module/types.py @@ -19,7 +19,7 @@ HeaterShakerModuleType = Literal["heaterShakerModuleType"] MagneticBlockType = Literal["magneticBlockType"] AbsorbanceReaderType = Literal["absorbanceReaderType"] -FlexStackerType = Literal["flexStackerType"] +FlexStackerModuleType = Literal["flexStackerModuleType"] ModuleType = Union[ MagneticModuleType, @@ -28,7 +28,7 @@ HeaterShakerModuleType, MagneticBlockType, AbsorbanceReaderType, - FlexStackerType, + FlexStackerModuleType, ] MagneticModuleModel = Literal["magneticModuleV1", "magneticModuleV2"] @@ -37,7 +37,7 @@ HeaterShakerModuleModel = Literal["heaterShakerModuleV1"] MagneticBlockModel = Literal["magneticBlockV1"] AbsorbanceReaderModel = Literal["absorbanceReaderV1"] -FlexStackerModel = Literal["flexStackerV1"] +FlexStackerModuleModel = Literal["flexStackerModuleV1"] ModuleModel = Union[ MagneticModuleModel, @@ -46,7 +46,7 @@ HeaterShakerModuleModel, MagneticBlockModel, AbsorbanceReaderModel, - FlexStackerModel, + FlexStackerModuleModel, ] ModuleSlotTransform = TypedDict(