diff --git a/.eslintignore b/.eslintignore index 27885c64ce6..1444a777aa1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -27,6 +27,7 @@ update-server/** robot-server/** notify-server/** shared-data/python/** +hardware-testing/** # app-testing don't format the json protocols app-testing/files diff --git a/.github/workflows/app-test-build-deploy.yaml b/.github/workflows/app-test-build-deploy.yaml index f50e4c0ad9e..dc93eae9c9a 100644 --- a/.github/workflows/app-test-build-deploy.yaml +++ b/.github/workflows/app-test-build-deploy.yaml @@ -230,7 +230,7 @@ jobs: echo "Configuring project, bucket, and folder for ot3" echo "project=ot3" >> $GITHUB_OUTPUT echo "bucket=${{env._APP_DEPLOY_BUCKET_OT3}}" >> $GITHUB_OUTPUT - echo "folder=${{env._APP_DEPLOY_BUCKET_OT3}}" >> $GITHUB_OUTPUT + echo "folder=${{env._APP_DEPLOY_FOLDER_OT3}}" >> $GITHUB_OUTPUT fi - uses: 'actions/checkout@v3' with: diff --git a/.github/workflows/ll-test-build-deploy.yaml b/.github/workflows/ll-test-build-deploy.yaml index 75e907af97f..e2e33d54146 100644 --- a/.github/workflows/ll-test-build-deploy.yaml +++ b/.github/workflows/ll-test-build-deploy.yaml @@ -116,6 +116,7 @@ jobs: build-ll: name: 'build labware library artifact' needs: ['js-unit-test'] + timeout-minutes: 30 runs-on: 'ubuntu-20.04' if: github.event_name != 'pull_request' steps: diff --git a/api-client/src/deck_configuration/__stubs__/index.ts b/api-client/src/deck_configuration/__stubs__/index.ts deleted file mode 100644 index 5f2bd147a0e..00000000000 --- a/api-client/src/deck_configuration/__stubs__/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { v4 as uuidv4 } from 'uuid' - -import { - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, -} from '@opentrons/shared-data' - -import type { Fixture } from '@opentrons/shared-data' - -export const DECK_CONFIG_STUB: { [fixtureLocation: string]: Fixture } = { - A1: { - fixtureLocation: 'A1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), - }, - B1: { - fixtureLocation: 'B1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), - }, - C1: { - fixtureLocation: 'C1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), - }, - D1: { - fixtureLocation: 'D1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), - }, - A2: { - fixtureLocation: 'A2', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), - }, - B2: { - fixtureLocation: 'B2', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), - }, - C2: { - fixtureLocation: 'C2', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), - }, - D2: { - fixtureLocation: 'D2', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), - }, - A3: { - fixtureLocation: 'A3', - loadName: TRASH_BIN_LOAD_NAME, - fixtureId: uuidv4(), - }, - B3: { - fixtureLocation: 'B3', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), - }, - C3: { - fixtureLocation: 'C3', - loadName: STAGING_AREA_LOAD_NAME, - fixtureId: uuidv4(), - }, - D3: { - fixtureLocation: 'D3', - loadName: WASTE_CHUTE_LOAD_NAME, - fixtureId: uuidv4(), - }, -} diff --git a/api-client/src/deck_configuration/createDeckConfiguration.ts b/api-client/src/deck_configuration/createDeckConfiguration.ts deleted file mode 100644 index 09a2f3b73d7..00000000000 --- a/api-client/src/deck_configuration/createDeckConfiguration.ts +++ /dev/null @@ -1,29 +0,0 @@ -// import { POST, request } from '../request' -import { DECK_CONFIG_STUB } from './__stubs__' - -import type { DeckConfiguration } from '@opentrons/shared-data' -// import type { ResponsePromise } from '../request' -import type { HostConfig } from '../types' - -// TODO(bh, 2023-09-26): uncomment and remove deck config stub when backend api is ready -// export function createDeckConfiguration( -// config: HostConfig, -// data: DeckConfiguration -// ): ResponsePromise { -// return request( -// POST, -// `/deck_configuration`, -// { data }, -// config -// ) -// } - -export function createDeckConfiguration( - config: HostConfig, - data: DeckConfiguration -): Promise<{ data: DeckConfiguration }> { - data.forEach(fixture => { - DECK_CONFIG_STUB[fixture.fixtureLocation] = fixture - }) - return Promise.resolve({ data: Object.values(DECK_CONFIG_STUB) }) -} diff --git a/api-client/src/deck_configuration/deleteDeckConfiguration.ts b/api-client/src/deck_configuration/deleteDeckConfiguration.ts deleted file mode 100644 index e3689f01559..00000000000 --- a/api-client/src/deck_configuration/deleteDeckConfiguration.ts +++ /dev/null @@ -1,30 +0,0 @@ -// import { DELETE, request } from '../request' -import { DECK_CONFIG_STUB } from './__stubs__' - -import type { Fixture } from '@opentrons/shared-data' -// import type { ResponsePromise } from '../request' -import type { EmptyResponse, HostConfig } from '../types' - -// TODO(bh, 2023-09-26): uncomment and remove deck config stub when backend api is ready -// export function deleteDeckConfiguration( -// config: HostConfig, -// data: Fixture -// ): ResponsePromise { -// const { fixtureLocation, ...rest } = data -// return request }>( -// DELETE, -// `/deck_configuration/${fixtureLocation}`, -// { data: rest }, -// config -// ) -// } - -export function deleteDeckConfiguration( - config: HostConfig, - data: Fixture -): Promise { - const { fixtureLocation } = data - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete DECK_CONFIG_STUB[fixtureLocation] - return Promise.resolve({ data: null }) -} diff --git a/api-client/src/deck_configuration/getDeckConfiguration.ts b/api-client/src/deck_configuration/getDeckConfiguration.ts index bc8c556e255..900f5e381e9 100644 --- a/api-client/src/deck_configuration/getDeckConfiguration.ts +++ b/api-client/src/deck_configuration/getDeckConfiguration.ts @@ -1,19 +1,16 @@ -// import { GET, request } from '../request' -import { DECK_CONFIG_STUB } from './__stubs__' +import { GET, request } from '../request' -import type { DeckConfiguration } from '@opentrons/shared-data' -// import type { ResponsePromise } from '../request' +import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' - -// TODO(bh, 2023-09-26): uncomment and remove deck config stub when backend api is ready -// export function getDeckConfiguration( -// config: HostConfig -// ): ResponsePromise { -// return request(GET, `/deck_configuration`, null, config) -// } +import type { DeckConfigurationResponse } from './types' export function getDeckConfiguration( config: HostConfig -): Promise<{ data: DeckConfiguration }> { - return Promise.resolve({ data: Object.values(DECK_CONFIG_STUB) }) +): ResponsePromise { + return request( + GET, + `/deck_configuration`, + null, + config + ) } diff --git a/api-client/src/deck_configuration/index.ts b/api-client/src/deck_configuration/index.ts index c22cba0ae78..3da16feea96 100644 --- a/api-client/src/deck_configuration/index.ts +++ b/api-client/src/deck_configuration/index.ts @@ -1,4 +1,7 @@ -export { createDeckConfiguration } from './createDeckConfiguration' -export { deleteDeckConfiguration } from './deleteDeckConfiguration' export { getDeckConfiguration } from './getDeckConfiguration' export { updateDeckConfiguration } from './updateDeckConfiguration' + +export type { + DeckConfigurationResponse, + UpdateDeckConfigurationRequest, +} from './types' diff --git a/api-client/src/deck_configuration/types.ts b/api-client/src/deck_configuration/types.ts new file mode 100644 index 00000000000..6396df3aeaf --- /dev/null +++ b/api-client/src/deck_configuration/types.ts @@ -0,0 +1,14 @@ +import type { DeckConfiguration } from '@opentrons/shared-data' + +export interface UpdateDeckConfigurationRequest { + data: { + cutoutFixtures: DeckConfiguration + } +} + +export interface DeckConfigurationResponse { + data: { + cutoutFixtures: DeckConfiguration + lastUpdatedAt: string + } +} diff --git a/api-client/src/deck_configuration/updateDeckConfiguration.ts b/api-client/src/deck_configuration/updateDeckConfiguration.ts index a02fb1af4b0..236aef59904 100644 --- a/api-client/src/deck_configuration/updateDeckConfiguration.ts +++ b/api-client/src/deck_configuration/updateDeckConfiguration.ts @@ -1,32 +1,21 @@ -import { v4 as uuidv4 } from 'uuid' +import { PUT, request } from '../request' -// import { PATCH, request } from '../request' -import { DECK_CONFIG_STUB } from './__stubs__' - -import type { Fixture } from '@opentrons/shared-data' -// import type { ResponsePromise } from '../request' +import type { DeckConfiguration } from '@opentrons/shared-data' +import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' - -// TODO(bh, 2023-09-26): uncomment and remove deck config stub when backend api is ready -// export function updateDeckConfiguration( -// config: HostConfig, -// data: Omit -// ): ResponsePromise { -// const { fixtureLocation, ...rest } = data -// return request }>( -// PATCH, -// `/deck_configuration/${fixtureLocation}`, -// { data: rest }, -// config -// ) -// } +import type { + DeckConfigurationResponse, + UpdateDeckConfigurationRequest, +} from './types' export function updateDeckConfiguration( config: HostConfig, - data: Omit -): Promise<{ data: Fixture }> { - const { fixtureLocation } = data - const fixtureId = uuidv4() - DECK_CONFIG_STUB[fixtureLocation] = { ...data, fixtureId } - return Promise.resolve({ data: DECK_CONFIG_STUB[fixtureLocation] }) + deckConfig: DeckConfiguration +): ResponsePromise { + return request( + PUT, + '/deck_configuration', + { data: { cutoutFixtures: deckConfig } }, + config + ) } diff --git a/api-client/src/protocols/__tests__/utils.test.ts b/api-client/src/protocols/__tests__/utils.test.ts index f86532d7359..c9edcae0068 100644 --- a/api-client/src/protocols/__tests__/utils.test.ts +++ b/api-client/src/protocols/__tests__/utils.test.ts @@ -10,17 +10,10 @@ import { parseLiquidsInLoadOrder, parseLabwareInfoByLiquidId, parseInitialLoadedLabwareByAdapter, - parseInitialLoadedFixturesByCutout, } from '../utils' import { simpleAnalysisFileFixture } from '../__fixtures__' -import { - LoadFixtureRunTimeCommand, - RunTimeCommand, - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, -} from '@opentrons/shared-data' +import { RunTimeCommand } from '@opentrons/shared-data' const mockRunTimeCommands: RunTimeCommand[] = simpleAnalysisFileFixture.commands as any const mockLoadLiquidRunTimeCommands = [ @@ -366,53 +359,6 @@ describe('parseInitialLoadedModulesBySlot', () => { ) }) }) -describe('parseInitialLoadedFixturesByCutout', () => { - it('returns fixtures loaded in cutouts', () => { - const loadFixtureCommands: LoadFixtureRunTimeCommand[] = [ - { - id: 'fakeId1', - commandType: 'loadFixture', - params: { - loadName: STAGING_AREA_LOAD_NAME, - location: { cutout: 'B3' }, - }, - createdAt: 'fake_timestamp', - startedAt: 'fake_timestamp', - completedAt: 'fake_timestamp', - status: 'succeeded', - }, - { - id: 'fakeId2', - commandType: 'loadFixture', - params: { loadName: WASTE_CHUTE_LOAD_NAME, location: { cutout: 'D3' } }, - createdAt: 'fake_timestamp', - startedAt: 'fake_timestamp', - completedAt: 'fake_timestamp', - status: 'succeeded', - }, - { - id: 'fakeId3', - commandType: 'loadFixture', - params: { - loadName: STANDARD_SLOT_LOAD_NAME, - location: { cutout: 'C3' }, - }, - createdAt: 'fake_timestamp', - startedAt: 'fake_timestamp', - completedAt: 'fake_timestamp', - status: 'succeeded', - }, - ] - const expected = { - B3: loadFixtureCommands[0], - D3: loadFixtureCommands[1], - C3: loadFixtureCommands[2], - } - expect(parseInitialLoadedFixturesByCutout(loadFixtureCommands)).toEqual( - expected - ) - }) -}) describe('parseLiquidsInLoadOrder', () => { it('returns liquids in loaded order', () => { const expected = [ diff --git a/api-client/src/protocols/utils.ts b/api-client/src/protocols/utils.ts index e8f19c42a7e..fefa2fab7f5 100644 --- a/api-client/src/protocols/utils.ts +++ b/api-client/src/protocols/utils.ts @@ -16,6 +16,7 @@ import type { ModuleModel, PipetteName, RunTimeCommand, + AddressableAreaName, } from '@opentrons/shared-data' interface PipetteNamesByMount { @@ -228,6 +229,7 @@ export function parseInitialLoadedModulesBySlot( export interface LoadedFixturesBySlot { [slotName: string]: LoadFixtureRunTimeCommand } +// TODO(bh, 2023-11-09): remove this util, there will be no loadFixture command export function parseInitialLoadedFixturesByCutout( commands: RunTimeCommand[] ): LoadedFixturesBySlot { @@ -244,6 +246,63 @@ export function parseInitialLoadedFixturesByCutout( ) } +export function parseAllAddressableAreas( + commands: RunTimeCommand[] +): AddressableAreaName[] { + return commands.reduce((acc, command) => { + if ( + command.commandType === 'moveLabware' && + command.params.newLocation !== 'offDeck' && + 'slotName' in command.params.newLocation && + !acc.includes(command.params.newLocation.slotName as AddressableAreaName) + ) { + return [ + ...acc, + command.params.newLocation.slotName as AddressableAreaName, + ] + } else if ( + command.commandType === 'moveLabware' && + command.params.newLocation !== 'offDeck' && + 'addressableAreaName' in command.params.newLocation && + !acc.includes( + command.params.newLocation.addressableAreaName as AddressableAreaName + ) + ) { + return [ + ...acc, + command.params.newLocation.addressableAreaName as AddressableAreaName, + ] + } else if ( + (command.commandType === 'loadLabware' || + command.commandType === 'loadModule') && + command.params.location !== 'offDeck' && + 'slotName' in command.params.location && + !acc.includes(command.params.location.slotName as AddressableAreaName) + ) { + return [...acc, command.params.location.slotName as AddressableAreaName] + } else if ( + command.commandType === 'loadLabware' && + command.params.location !== 'offDeck' && + 'addressableAreaName' in command.params.location && + !acc.includes( + command.params.location.addressableAreaName as AddressableAreaName + ) + ) { + return [ + ...acc, + command.params.location.addressableAreaName as AddressableAreaName, + ] + } else if ( + command.commandType === 'moveToAddressableArea' && + !acc.includes(command.params.addressableAreaName as AddressableAreaName) + ) { + return [...acc, command.params.addressableAreaName as AddressableAreaName] + } else { + return acc + } + }, []) +} + export interface LiquidsById { [liquidId: string]: { displayName: string diff --git a/api/Makefile b/api/Makefile index adff7704fd9..c6e78d04939 100755 --- a/api/Makefile +++ b/api/Makefile @@ -184,7 +184,8 @@ push-no-restart-ot3: sdist echo $(sdist_file) $(call push-python-sdist,$(host),$(ssh_key),$(ssh_opts),$(sdist_file),/opt/opentrons-robot-server,opentrons,src,,$(version_file)) ssh $(ssh_helper) root@$(host) "mount -o remount,rw / && mkdir -p /usr/local/bin" - scp $(ssh_helper) ./src/opentrons/hardware_control/scripts/{ot3repl,ot3gripper} root@$(host):/usr/local/bin/ + scp $(ssh_helper) ./src/opentrons/hardware_control/scripts/ot3repl root@$(host):/usr/local/bin/ + scp $(ssh_helper) ./src/opentrons/hardware_control/scripts/ot3gripper root@$(host):/usr/local/bin/ ssh $(ssh_helper) root@$(host) "mount -o remount,ro /" .PHONY: push-ot3 diff --git a/api/docs/v2/conf.py b/api/docs/v2/conf.py index 29524982522..cb896e72d89 100644 --- a/api/docs/v2/conf.py +++ b/api/docs/v2/conf.py @@ -96,13 +96,10 @@ # setup the code block substitution extension to auto-update apiLevel extensions += ['sphinx-prompt', 'sphinx_substitution_extensions'] -# get the max API level -from opentrons.protocol_api import MAX_SUPPORTED_VERSION # noqa -max_apiLevel = str(MAX_SUPPORTED_VERSION) - # use rst_prolog to hold the subsitution +# update the apiLevel value whenever a new minor version is released rst_prolog = f""" -.. |apiLevel| replace:: {max_apiLevel} +.. |apiLevel| replace:: 2.15 .. |release| replace:: {release} """ diff --git a/api/docs/v2/new_protocol_api.rst b/api/docs/v2/new_protocol_api.rst index 9ddd0ca6407..5166d53f035 100644 --- a/api/docs/v2/new_protocol_api.rst +++ b/api/docs/v2/new_protocol_api.rst @@ -17,7 +17,7 @@ Protocols and Instruments .. autoclass:: opentrons.protocol_api.InstrumentContext :members: - :exclude-members: delay + :exclude-members: delay, configure_nozzle_layout, prepare_to_aspirate .. autoclass:: opentrons.protocol_api.Liquid diff --git a/api/release-notes-internal.md b/api/release-notes-internal.md index 07c68ac4749..6bba1e17950 100644 --- a/api/release-notes-internal.md +++ b/api/release-notes-internal.md @@ -4,52 +4,27 @@ For more details about this release, please see the full [technical change log][ --- -# Internal Release 0.14.0 +# Internal Release 1.1.0 ## New Stuff In This Release -- Return tip heights and some other pipette behaviors are now properly executed based on the kind of tip being used -- Release Flex robot software builds are now cryptographically signed. If you run a release build, you can only install other properly signed release builds. Note that if the robot was previously on a non-release build this won't latch; remove the update server config file at ``/var/lib/otupdate/config.json`` to go back to signed builds only. -- Error handling has been overhauled; all errors now display with an error code for easier reporting. Many of those error codes are the 4000 catchall still but this will improve over time. -- If there's an error during the post-run cleanup steps, where the robot homes and drops tips, the run should no longer get stuck in a permanent "finishing" state. It should get marked as failed. -- Further updates to Flex motion control parameters from hardware testing for both gantry and plunger speeds and acceleration -- Pipette overpressure detection is now integrated. -- All instrument flows should now show errors if they occur instead of skipping a step -- Fixes to several incorrect status displays in ODD (i.e. protocols skipping the full-color outcome splash) -- Robot can now handle json protocol v7 -- Support for PVT (v1.1) grippers -- Update progress should get displayed after restart for firmware updates -- Removed `use_pick_up_location_lpc_offset` and `use_drop_location_lpc_offset` from `protocol_context.move_labware` arguments. So they should be removed from any protocols that used them. This change also requires resetting the protocol run database on the robot. -- Added 'contextual' gripper offsets to deck, labware and module definitions. So, any labware movement offsets that were previously being specified in the protocol should now be removed or adjusted or they will get added twice. - - -## Big Things That Don't Work Yet So Don't Report Bugs About Them - -### Robot Control -- Pipette pressure sensing for liquid-level sensing purposes -- Labware pick up failure with gripper -- E-stop integrated handling especially with modules - -## Big Things That Do Work Please Do Report Bugs About Them -### Robot Control -- Protocol behavior -- Labware movement between slots/modules, both manual and with gripper, from python protocols -- Labware drop/gripper crash errors, but they're very insensitive -- Pipette and gripper automated offset calibration -- Network connectivity and discoverability -- Firmware update for all devices -- Cancelling a protocol run. We're even more sure we fixed this so definitely tell us if it's not. -- USB connectivity -- Stall detection firing basically ever unless you clearly ran into something - -### ODD -- Protocol execution including end-of-protocol screen -- Protocol run monitoring -- Attach and calibrate -- Network connection management, including viewing IP addresses and connecting to wifi networks -- Automatic updates of robot software when new internal releases are created -- Chrome remote devtools - if you enable them and then use Chrome to go to robotip:9223 you'll get devtools -- After a while, the ODD should go into idle; if you touch it, it will come back online - - - +This is a tracking internal release coming off of the edge branch to contain rapid dev on new features for 7.1.0. Features will change drastically between successive alphas even over the course of the day. For this reason, these release notes will not be in their usual depth. + +The biggest new features, however, are +- There is a new protocol API version, 2.16, which changes how the default trash is loaded and gates features like partial tip pickup and waste chute usage: + - Protocols do not load a trash by default. To load the normal trash, load ``opentrons_1_trash_3200ml_fixed`` in slot ``A3``. + - But also you can load it in any other edge slot if you want (columns 1 and 3). + - Protocols can load trash chutes; the details of exactly how this works are still in flux. + - Protocols can configure their 96 and 8 channel pipettes to pick up only a subset of tips using ``configure_nozzle_layout``. +- Support for json protocol V8 and command V8, which adds JSON protocol support for the above features. +- ODD support for rendering the above features in protocols +- ODD support for configuring the loaded deck fixtures like trash chutes +- Labware position check now uses the calibration probe (the same one used for pipette and module calibration) instead of a tip; this should increase the accuracy of LPC. +- Support for P1000S v3.6 +- Updated liquid handling functions for all 96 channel pipettes + +## Known Issues + +- The ``MoveToAddressableArea`` command will noop. This means that all commands that use the movable trash bin will not "move to the trash bin". The command will analyze successfully. +- The deck configuration on the robot is not persistent, this means that between boots of a robot, you must PUT a deck configuration on the robot via HTTP. + diff --git a/api/src/opentrons/cli/analyze.py b/api/src/opentrons/cli/analyze.py index 9f5af67c584..4ee9f6507af 100644 --- a/api/src/opentrons/cli/analyze.py +++ b/api/src/opentrons/cli/analyze.py @@ -79,7 +79,7 @@ async def _analyze( runner = await create_simulating_runner( robot_type=protocol_source.robot_type, protocol_config=protocol_source.config ) - analysis = await runner.run(protocol_source) + analysis = await runner.run(deck_configuration=[], protocol_source=protocol_source) if json_output: results = AnalyzeResults.construct( @@ -145,10 +145,16 @@ class AnalyzeResults(BaseModel): See robot-server's analysis models for field documentation. """ + # We want to unify this local analysis model with the one that robot-server returns. + # Until that happens, we need to keep these fields in sync manually. + + # Fields that are currently unique to this local analysis module, missing from robot-server: createdAt: datetime files: List[ProtocolFile] config: Union[JsonConfig, PythonConfig] metadata: Dict[str, Any] + + # Fields that should match robot-server: robotType: RobotType commands: List[Command] labware: List[LoadedLabware] diff --git a/api/src/opentrons/config/containers/__init__.py b/api/src/opentrons/config/containers/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/api/src/opentrons/config/containers/default-containers.json b/api/src/opentrons/config/containers/default-containers.json deleted file mode 100644 index 44824a024a4..00000000000 --- a/api/src/opentrons/config/containers/default-containers.json +++ /dev/null @@ -1,27097 +0,0 @@ -{ - "containers": { - "temperature-plate": { - "origin-offset": { - "x": 11.24, - "y": 14.34, - "z": 97 - }, - "locations":{} - }, - - "tube-rack-5ml-96": { - "locations": { - "A1": { - "y": 0, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B1": { - "y": 0, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C1": { - "y": 0, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D1": { - "y": 0, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E1": { - "y": 0, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F1": { - "y": 0, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G1": { - "y": 0, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H1": { - "y": 0, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A2": { - "y": 18, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B2": { - "y": 18, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C2": { - "y": 18, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D2": { - "y": 18, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E2": { - "y": 18, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F2": { - "y": 18, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G2": { - "y": 18, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H2": { - "y": 18, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A3": { - "y": 36, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B3": { - "y": 36, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C3": { - "y": 36, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D3": { - "y": 36, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E3": { - "y": 36, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F3": { - "y": 36, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G3": { - "y": 36, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H3": { - "y": 36, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A4": { - "y": 54, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B4": { - "y": 54, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C4": { - "y": 54, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D4": { - "y": 54, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E4": { - "y": 54, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F4": { - "y": 54, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G4": { - "y": 54, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H4": { - "y": 54, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A5": { - "y": 72, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B5": { - "y": 72, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C5": { - "y": 72, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D5": { - "y": 72, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E5": { - "y": 72, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F5": { - "y": 72, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G5": { - "y": 72, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H5": { - "y": 72, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A6": { - "y": 90, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B6": { - "y": 90, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C6": { - "y": 90, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D6": { - "y": 90, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E6": { - "y": 90, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F6": { - "y": 90, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G6": { - "y": 90, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H6": { - "y": 90, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A7": { - "y": 108, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B7": { - "y": 108, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C7": { - "y": 108, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D7": { - "y": 108, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E7": { - "y": 108, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F7": { - "y": 108, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G7": { - "y": 108, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H7": { - "y": 108, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A8": { - "y": 126, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B8": { - "y": 126, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C8": { - "y": 126, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D8": { - "y": 126, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E8": { - "y": 126, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F8": { - "y": 126, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G8": { - "y": 126, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H8": { - "y": 126, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A9": { - "y": 144, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B9": { - "y": 144, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C9": { - "y": 144, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D9": { - "y": 144, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E9": { - "y": 144, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F9": { - "y": 144, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G9": { - "y": 144, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H9": { - "y": 144, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A10": { - "y": 162, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B10": { - "y": 162, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C10": { - "y": 162, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D10": { - "y": 162, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E10": { - "y": 162, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F10": { - "y": 162, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G10": { - "y": 162, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H10": { - "y": 162, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A11": { - "y": 180, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B11": { - "y": 180, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C11": { - "y": 180, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D11": { - "y": 180, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E11": { - "y": 180, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F11": { - "y": 180, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G11": { - "y": 180, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H11": { - "y": 180, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - - "A12": { - "y": 198, - "x": 0, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "B12": { - "y": 198, - "x": 18, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "C12": { - "y": 198, - "x": 36, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "D12": { - "y": 198, - "x": 54, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "E12": { - "y": 198, - "x": 72, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "F12": { - "y": 198, - "x": 90, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "G12": { - "y": 198, - "x": 108, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - }, - "H12": { - "y": 198, - "x": 126, - "z": 0, - "depth": 72, - "diameter": 15, - "total-liquid-volume": 5000 - } - - } - }, - - "tube-rack-2ml-9x9": { - "locations": { - "A1": { - "y": 0, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B1": { - "y": 0, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C1": { - "y": 0, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D1": { - "y": 0, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E1": { - "y": 0, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F1": { - "y": 0, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G1": { - "y": 0, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H1": { - "y": 0, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I1": { - "y": 0, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A2": { - "y": 14.75, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B2": { - "y": 14.75, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C2": { - "y": 14.75, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D2": { - "y": 14.75, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E2": { - "y": 14.75, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F2": { - "y": 14.75, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G2": { - "y": 14.75, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H2": { - "y": 14.75, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I2": { - "y": 14.75, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A3": { - "y": 29.5, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B3": { - "y": 29.5, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C3": { - "y": 29.5, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D3": { - "y": 29.5, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E3": { - "y": 29.5, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F3": { - "y": 29.5, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G3": { - "y": 29.5, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H3": { - "y": 29.5, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I3": { - "y": 29.5, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A4": { - "y": 44.25, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B4": { - "y": 44.25, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C4": { - "y": 44.25, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D4": { - "y": 44.25, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E4": { - "y": 44.25, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F4": { - "y": 44.25, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G4": { - "y": 44.25, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H4": { - "y": 44.25, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I4": { - "y": 44.25, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A5": { - "y": 59, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B5": { - "y": 59, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C5": { - "y": 59, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D5": { - "y": 59, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E5": { - "y": 59, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F5": { - "y": 59, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G5": { - "y": 59, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H5": { - "y": 59, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I5": { - "y": 59, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A6": { - "y": 73.75, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B6": { - "y": 73.75, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C6": { - "y": 73.75, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D6": { - "y": 73.75, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E6": { - "y": 73.75, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F6": { - "y": 73.75, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G6": { - "y": 73.75, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H6": { - "y": 73.75, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I6": { - "y": 73.75, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A7": { - "y": 88.5, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B7": { - "y": 88.5, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C7": { - "y": 88.5, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D7": { - "y": 88.5, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E7": { - "y": 88.5, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F7": { - "y": 88.5, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G7": { - "y": 88.5, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H7": { - "y": 88.5, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I7": { - "y": 88.5, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A8": { - "y": 103.25, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B8": { - "y": 103.25, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C8": { - "y": 103.25, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D8": { - "y": 103.25, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E8": { - "y": 103.25, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F8": { - "y": 103.25, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G8": { - "y": 103.25, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H8": { - "y": 103.25, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I8": { - "y": 103.25, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - - "A9": { - "y": 118, - "x": 0, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "B9": { - "y": 118, - "x": 14.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "C9": { - "y": 118, - "x": 29.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "D9": { - "y": 118, - "x": 44.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "E9": { - "y": 118, - "x": 59, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "F9": { - "y": 118, - "x": 73.75, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "G9": { - "y": 118, - "x": 88.5, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "H9": { - "y": 118, - "x": 103.25, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - }, - "I9": { - "y": 118, - "x": 118, - "z": 0, - "depth": 45, - "diameter": 10, - "total-liquid-volume": 2000 - } - } - }, - "96-well-plate-20mm": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 20.2, - "diameter": 5.46, - "total-liquid-volume": 300 - } - } - }, - "6-well-plate": { - "origin-offset": { - "x": 23.16, - "y": 24.76 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 22.5, - "total-liquid-volume": 16800 - }, - "B1": { - "x": 39.12, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 22.5, - "total-liquid-volume": 16800 - }, - "A2": { - "x": 0, - "y": 39.12, - "z": 0, - "depth": 17.4, - "diameter": 22.5, - "total-liquid-volume": 16800 - }, - "B2": { - "x": 39.12, - "y": 39.12, - "z": 0, - "depth": 17.4, - "diameter": 22.5, - "total-liquid-volume": 16800 - }, - "A3": { - "x": 0, - "y": 78.24, - "z": 0, - "depth": 17.4, - "diameter": 22.5, - "total-liquid-volume": 16800 - }, - "B3": { - "x": 39.12, - "y": 78.24, - "z": 0, - "depth": 17.4, - "diameter": 22.5, - "total-liquid-volume": 16800 - } - } - }, - "12-well-plate": { - "origin-offset": { - "x": 16.79, - "y": 24.94 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "B1": { - "x": 26.01, - "y": 0, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "C1": { - "x": 52.02, - "y": 0, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "A2": { - "x": 0, - "y": 26.01, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "B2": { - "x": 26.01, - "y": 26.01, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "C2": { - "x": 52.02, - "y": 26.01, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "A3": { - "x": 0, - "y": 52.02, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "B3": { - "x": 26.01, - "y": 52.02, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "C3": { - "x": 52.02, - "y": 52.02, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "A4": { - "x": 0, - "y": 78.03, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "B4": { - "x": 26.01, - "y": 78.03, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - }, - "C4": { - "x": 52.02, - "y": 78.03, - "z": 0, - "depth": 17.53, - "diameter": 22.5, - "total-liquid-volume": 6900 - } - } - }, - "24-well-plate": { - "origin-offset": { - "x": 13.67, - "y": 15 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "B1": { - "x": 19.3, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "C1": { - "x": 38.6, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "D1": { - "x": 57.9, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "A2": { - "x": 0, - "y": 19.3, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "B2": { - "x": 19.3, - "y": 19.3, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "C2": { - "x": 38.6, - "y": 19.3, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "D2": { - "x": 57.9, - "y": 19.3, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "A3": { - "x": 0, - "y": 38.6, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "B3": { - "x": 19.3, - "y": 38.6, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "C3": { - "x": 38.6, - "y": 38.6, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "D3": { - "x": 57.9, - "y": 38.6, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "A4": { - "x": 0, - "y": 57.9, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "B4": { - "x": 19.3, - "y": 57.9, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "C4": { - "x": 38.6, - "y": 57.9, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "D4": { - "x": 57.9, - "y": 57.9, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "A5": { - "x": 0, - "y": 77.2, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "B5": { - "x": 19.3, - "y": 77.2, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "C5": { - "x": 38.6, - "y": 77.2, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "D5": { - "x": 57.9, - "y": 77.2, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "A6": { - "x": 0, - "y": 96.5, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "B6": { - "x": 19.3, - "y": 96.5, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "C6": { - "x": 38.6, - "y": 96.5, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - }, - "D6": { - "x": 57.9, - "y": 96.5, - "z": 0, - "depth": 17.4, - "diameter": 16, - "total-liquid-volume": 1900 - } - } - }, - "48-well-plate": { - "origin-offset": { - "x": 10.08, - "y": 18.16 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B1": { - "x": 13.08, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C1": { - "x": 26.16, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D1": { - "x": 39.24, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E1": { - "x": 52.32, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F1": { - "x": 65.4, - "y": 0, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "A2": { - "x": 0, - "y": 13.08, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B2": { - "x": 13.08, - "y": 13.08, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C2": { - "x": 26.16, - "y": 13.08, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D2": { - "x": 39.24, - "y": 13.08, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E2": { - "x": 52.32, - "y": 13.08, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F2": { - "x": 65.4, - "y": 13.08, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "A3": { - "x": 0, - "y": 26.16, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B3": { - "x": 13.08, - "y": 26.16, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C3": { - "x": 26.16, - "y": 26.16, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D3": { - "x": 39.24, - "y": 26.16, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E3": { - "x": 52.32, - "y": 26.16, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F3": { - "x": 65.4, - "y": 26.16, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "A4": { - "x": 0, - "y": 39.24, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B4": { - "x": 13.08, - "y": 39.24, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C4": { - "x": 26.16, - "y": 39.24, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D4": { - "x": 39.24, - "y": 39.24, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E4": { - "x": 52.32, - "y": 39.24, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F4": { - "x": 65.4, - "y": 39.24, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "A5": { - "x": 0, - "y": 52.32, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B5": { - "x": 13.08, - "y": 52.32, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C5": { - "x": 26.16, - "y": 52.32, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D5": { - "x": 39.24, - "y": 52.32, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E5": { - "x": 52.32, - "y": 52.32, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F5": { - "x": 65.4, - "y": 52.32, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "A6": { - "x": 0, - "y": 65.4, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B6": { - "x": 13.08, - "y": 65.4, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C6": { - "x": 26.16, - "y": 65.4, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D6": { - "x": 39.24, - "y": 65.4, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E6": { - "x": 52.32, - "y": 65.4, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F6": { - "x": 65.4, - "y": 65.4, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "A7": { - "x": 0, - "y": 78.48, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B7": { - "x": 13.08, - "y": 78.48, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C7": { - "x": 26.16, - "y": 78.48, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D7": { - "x": 39.24, - "y": 78.48, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E7": { - "x": 52.32, - "y": 78.48, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F7": { - "x": 65.4, - "y": 78.48, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "A8": { - "x": 0, - "y": 91.56, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "B8": { - "x": 13.08, - "y": 91.56, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "C8": { - "x": 26.16, - "y": 91.56, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "D8": { - "x": 39.24, - "y": 91.56, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "E8": { - "x": 52.32, - "y": 91.56, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - }, - "F8": { - "x": 65.4, - "y": 91.56, - "z": 0, - "depth": 17.4, - "diameter": 11.25, - "total-liquid-volume": 950 - } - } - }, - "trough-1row-25ml": { - "locations": { - "A1": { - "x": 42.75, - "y": 63.875, - "z": 0, - "depth": 26, - "diameter": 10, - "total-liquid-volume": 25000 - } - } - }, - "trough-1row-test": { - "locations": { - "A1": { - "x": 42.75, - "y": 63.875, - "z": 0, - "depth": 26, - "diameter": 10, - "total-liquid-volume": 25000 - } - } - }, - "hampton-1ml-deep-block": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 38, - "diameter": 7.5, - "total-liquid-volume": 1000 - } - } - }, - - "rigaku-compact-crystallization-plate": { - "origin-offset": { - "x": 9, - "y": 11 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 2.5, - "diameter": 2, - "total-liquid-volume": 6 - }, - - "A13": { - "x": 3.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B13": { - "x": 12.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C13": { - "x": 21.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D13": { - "x": 30.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E13": { - "x": 39.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F13": { - "x": 48.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G13": { - "x": 57.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H13": { - "x": 66.5, - "y": 3.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A14": { - "x": 3.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B14": { - "x": 12.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C14": { - "x": 21.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D14": { - "x": 30.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E14": { - "x": 39.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F14": { - "x": 48.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G14": { - "x": 57.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H14": { - "x": 66.5, - "y": 12.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A15": { - "x": 3.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B15": { - "x": 12.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C15": { - "x": 21.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D15": { - "x": 30.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E15": { - "x": 39.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F15": { - "x": 48.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G15": { - "x": 57.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H15": { - "x": 66.5, - "y": 21.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A16": { - "x": 3.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B16": { - "x": 12.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C16": { - "x": 21.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D16": { - "x": 30.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E16": { - "x": 39.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F16": { - "x": 48.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G16": { - "x": 57.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H16": { - "x": 66.5, - "y": 30.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A17": { - "x": 3.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B17": { - "x": 12.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C17": { - "x": 21.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D17": { - "x": 30.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E17": { - "x": 39.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F17": { - "x": 48.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G17": { - "x": 57.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H17": { - "x": 66.5, - "y": 39.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A18": { - "x": 3.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B18": { - "x": 12.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C18": { - "x": 21.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D18": { - "x": 30.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E18": { - "x": 39.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F18": { - "x": 48.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G18": { - "x": 57.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H18": { - "x": 66.5, - "y": 48.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A19": { - "x": 3.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B19": { - "x": 12.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C19": { - "x": 21.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D19": { - "x": 30.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E19": { - "x": 39.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F19": { - "x": 48.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G19": { - "x": 57.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H19": { - "x": 66.5, - "y": 57.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A20": { - "x": 3.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B20": { - "x": 12.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C20": { - "x": 21.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D20": { - "x": 30.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E20": { - "x": 39.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F20": { - "x": 48.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G20": { - "x": 57.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H20": { - "x": 66.5, - "y": 66.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A21": { - "x": 3.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B21": { - "x": 12.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C21": { - "x": 21.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D21": { - "x": 30.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E21": { - "x": 39.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F21": { - "x": 48.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G21": { - "x": 57.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H21": { - "x": 66.5, - "y": 75.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A22": { - "x": 3.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B22": { - "x": 12.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C22": { - "x": 21.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D22": { - "x": 30.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E22": { - "x": 39.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F22": { - "x": 48.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G22": { - "x": 57.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H22": { - "x": 66.5, - "y": 84.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A23": { - "x": 3.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B23": { - "x": 12.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C23": { - "x": 21.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D23": { - "x": 30.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E23": { - "x": 39.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F23": { - "x": 48.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G23": { - "x": 57.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H23": { - "x": 66.5, - "y": 93.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - - "A24": { - "x": 3.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "B24": { - "x": 12.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "C24": { - "x": 21.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "D24": { - "x": 30.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "E24": { - "x": 39.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "F24": { - "x": 48.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "G24": { - "x": 57.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - }, - "H24": { - "x": 66.5, - "y": 102.5, - "z": -6, - "depth": 6.5, - "diameter": 2.5, - "total-liquid-volume": 300 - } - } - }, - "alum-block-pcr-strips": { - - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A2": { - "x": 0, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B2": { - "x": 9, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C2": { - "x": 18, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D2": { - "x": 27, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E2": { - "x": 36, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F2": { - "x": 45, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G2": { - "x": 54, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H2": { - "x": 63, - "y": 117, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - } - } - }, - "T75-flask": { - "origin-offset": { - "x": 42.75, - "y": 63.875 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 163, - "diameter": 25, - "total-liquid-volume": 75000 - } - } - }, - "T25-flask": { - "origin-offset": { - "x": 42.75, - "y": 63.875 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 99, - "diameter": 18, - "total-liquid-volume": 25000 - } - } - }, - "magdeck": { - "origin-offset": { - "x": 0, - "y": 0 - }, - "locations": { - "A1": { - "x": 63.9, - "y": 42.8, - "z": 0, - "depth": 82.25 - } - } - }, - "tempdeck": { - "origin-offset": { - "x": 0, - "y": 0 - }, - "locations": { - "A1": { - "x": 63.9, - "y": 42.8, - "z": 0, - "depth": 80.09 - } - } - }, - "trash-box": { - "origin-offset": { - "x": 42.75, - "y": 63.875 - }, - "locations": { - "A1": { - "x": 60, - "y": 45, - "z": 0, - "depth": 40 - } - } - }, - "fixed-trash": { - "origin-offset": { - "x": 0, - "y": 0 - }, - "locations": { - "A1": { - "x": 80, - "y": 80, - "z": 5, - "depth": 58 - } - } - }, - "tall-fixed-trash": { - "origin-offset": { - "x": 0, - "y": 0 - }, - "locations": { - "A1": { - "x": 80, - "y": 80, - "z": 5, - "depth": 80 - } - } - }, - "wheaton_vial_rack": { - "origin-offset": { - "x": 9, - "y": 9 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 35, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 70, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 105, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "E1": { - "x": 140, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "F1": { - "x": 175, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "G1": { - "x": 210, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "H1": { - "x": 245, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "I1": { - "x": 280, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "J1": { - "x": 315, - "y": 0, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - - "A2": { - "x": 0, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 35, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 70, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 105, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "E2": { - "x": 140, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "F2": { - "x": 175, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "G2": { - "x": 210, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "H2": { - "x": 245, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "I2": { - "x": 280, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "J2": { - "x": 315, - "y": 35, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - - "A3": { - "x": 0, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 35, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 70, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 105, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "E3": { - "x": 140, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "F3": { - "x": 175, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "G3": { - "x": 210, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "H3": { - "x": 245, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "I3": { - "x": 280, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "J3": { - "x": 315, - "y": 70, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - - "A4": { - "x": 0, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 35, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 70, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 105, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "E4": { - "x": 140, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "F4": { - "x": 175, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "G4": { - "x": 210, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "H4": { - "x": 245, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "I4": { - "x": 280, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "J4": { - "x": 315, - "y": 105, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - - "A5": { - "x": 0, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 35, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 70, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 105, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "E5": { - "x": 140, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "F5": { - "x": 175, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "G5": { - "x": 210, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "H5": { - "x": 245, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "I5": { - "x": 280, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - }, - "J5": { - "x": 315, - "y": 140, - "z": 0, - "depth": 95, - "diameter": 18, - "total-liquid-volume": 2000 - } - } - }, - "tube-rack-80well": { - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 13.2, - "y": 0, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 26.5, - "y": 0, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 39.7, - "y": 0, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E1": { - "x": 52.9, - "y": 0, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A2": { - "x": 0, - "y": 13.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 13.2, - "y": 13.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 26.5, - "y": 13.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 39.7, - "y": 13.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E2": { - "x": 52.9, - "y": 13.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A3": { - "x": 0, - "y": 26.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 13.2, - "y": 26.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 26.5, - "y": 26.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 39.7, - "y": 26.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E3": { - "x": 52.9, - "y": 26.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A4": { - "x": 0, - "y": 39.7, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 13.2, - "y": 39.7, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 26.5, - "y": 39.7, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 39.7, - "y": 39.7, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E4": { - "x": 52.9, - "y": 39.7, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A5": { - "x": 0, - "y": 52.9, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 13.2, - "y": 52.9, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 26.5, - "y": 52.9, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 39.7, - "y": 52.9, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E5": { - "x": 52.9, - "y": 52.9, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A6": { - "x": 0, - "y": 66.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 13.2, - "y": 66.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 26.5, - "y": 66.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 39.7, - "y": 66.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E6": { - "x": 52.9, - "y": 66.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A7": { - "x": 0, - "y": 79.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B7": { - "x": 13.2, - "y": 79.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C7": { - "x": 26.5, - "y": 79.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D7": { - "x": 39.7, - "y": 79.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E7": { - "x": 52.9, - "y": 79.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A8": { - "x": 0, - "y": 92.6, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B8": { - "x": 13.2, - "y": 92.6, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C8": { - "x": 26.5, - "y": 92.6, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D8": { - "x": 39.7, - "y": 92.6, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E8": { - "x": 52.9, - "y": 92.6, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A9": { - "x": 0, - "y": 105.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B9": { - "x": 13.2, - "y": 105.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C9": { - "x": 26.5, - "y": 105.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D9": { - "x": 39.7, - "y": 105.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E9": { - "x": 52.9, - "y": 105.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A10": { - "x": 0, - "y": 119.1, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B10": { - "x": 13.2, - "y": 119.1, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C10": { - "x": 26.5, - "y": 119.1, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D10": { - "x": 39.7, - "y": 119.1, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E10": { - "x": 52.9, - "y": 119.1, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A11": { - "x": 0, - "y": 132.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B11": { - "x": 13.2, - "y": 132.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C11": { - "x": 26.5, - "y": 132.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D11": { - "x": 39.7, - "y": 132.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E11": { - "x": 52.9, - "y": 132.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A12": { - "x": 0, - "y": 145.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B12": { - "x": 13.2, - "y": 145.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C12": { - "x": 26.5, - "y": 145.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D12": { - "x": 39.7, - "y": 145.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E12": { - "x": 52.9, - "y": 145.5, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A13": { - "x": 0, - "y": 158.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B13": { - "x": 13.2, - "y": 158.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C13": { - "x": 26.5, - "y": 158.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D13": { - "x": 39.7, - "y": 158.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E13": { - "x": 52.9, - "y": 158.8, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A14": { - "x": 0, - "y": 172, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B14": { - "x": 13.2, - "y": 172, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C14": { - "x": 26.5, - "y": 172, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D14": { - "x": 39.7, - "y": 172, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E14": { - "x": 52.9, - "y": 172, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A15": { - "x": 0, - "y": 185.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B15": { - "x": 13.2, - "y": 185.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C15": { - "x": 26.5, - "y": 185.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D15": { - "x": 39.7, - "y": 185.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E15": { - "x": 52.9, - "y": 185.2, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A16": { - "x": 0, - "y": 198.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B16": { - "x": 13.2, - "y": 198.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C16": { - "x": 26.5, - "y": 198.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D16": { - "x": 39.7, - "y": 198.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "E16": { - "x": 52.9, - "y": 198.4, - "z": 0, - "depth": 35, - "diameter": 6, - "total-liquid-volume": 2000 - } - } - }, - "point": { - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 0, - "total-liquid-volume": 1 - } - } - }, - "tiprack-10ul": { - "origin-offset": { - "x": 14.24, - "y": 14.54 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 56, - "diameter": 3.5 - } - } - }, - - "tiprack-10ul-H": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 60, - "diameter": 6.4 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 60, - "diameter": 6.4 - } - } - }, - - "tiprack-200ul": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "diameter": 3.5, - "depth": 60 - } - } - }, - "opentrons-tiprack-10ul": { - "origin-offset": { - "x": 10.77, - "y": 11.47 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B1": { - "x": 9, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C1": { - "x": 18, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D1": { - "x": 27, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E1": { - "x": 36, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F1": { - "x": 45, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G1": { - "x": 54, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H1": { - "x": 63, - "y": 0, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A2": { - "x": 0, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B2": { - "x": 9, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C2": { - "x": 18, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D2": { - "x": 27, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E2": { - "x": 36, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F2": { - "x": 45, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G2": { - "x": 54, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H2": { - "x": 63, - "y": 9, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A3": { - "x": 0, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B3": { - "x": 9, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C3": { - "x": 18, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D3": { - "x": 27, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E3": { - "x": 36, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F3": { - "x": 45, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G3": { - "x": 54, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H3": { - "x": 63, - "y": 18, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A4": { - "x": 0, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B4": { - "x": 9, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C4": { - "x": 18, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D4": { - "x": 27, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E4": { - "x": 36, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F4": { - "x": 45, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G4": { - "x": 54, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H4": { - "x": 63, - "y": 27, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A5": { - "x": 0, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B5": { - "x": 9, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C5": { - "x": 18, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D5": { - "x": 27, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E5": { - "x": 36, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F5": { - "x": 45, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G5": { - "x": 54, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H5": { - "x": 63, - "y": 36, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A6": { - "x": 0, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B6": { - "x": 9, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C6": { - "x": 18, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D6": { - "x": 27, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E6": { - "x": 36, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F6": { - "x": 45, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G6": { - "x": 54, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H6": { - "x": 63, - "y": 45, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A7": { - "x": 0, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B7": { - "x": 9, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C7": { - "x": 18, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D7": { - "x": 27, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E7": { - "x": 36, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F7": { - "x": 45, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G7": { - "x": 54, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H7": { - "x": 63, - "y": 54, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A8": { - "x": 0, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B8": { - "x": 9, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C8": { - "x": 18, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D8": { - "x": 27, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E8": { - "x": 36, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F8": { - "x": 45, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G8": { - "x": 54, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H8": { - "x": 63, - "y": 63, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A9": { - "x": 0, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B9": { - "x": 9, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C9": { - "x": 18, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D9": { - "x": 27, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E9": { - "x": 36, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F9": { - "x": 45, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G9": { - "x": 54, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H9": { - "x": 63, - "y": 72, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A10": { - "x": 0, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B10": { - "x": 9, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C10": { - "x": 18, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D10": { - "x": 27, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E10": { - "x": 36, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F10": { - "x": 45, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G10": { - "x": 54, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H10": { - "x": 63, - "y": 81, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A11": { - "x": 0, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B11": { - "x": 9, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C11": { - "x": 18, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D11": { - "x": 27, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E11": { - "x": 36, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F11": { - "x": 45, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G11": { - "x": 54, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H11": { - "x": 63, - "y": 90, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "A12": { - "x": 0, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "B12": { - "x": 9, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "C12": { - "x": 18, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "D12": { - "x": 27, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "E12": { - "x": 36, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "F12": { - "x": 45, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "G12": { - "x": 54, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - }, - "H12": { - "x": 63, - "y": 99, - "z": 25.46, - "diameter": 3.5, - "depth": 39.2 - } - } - }, - "opentrons-tiprack-300ul": { - "origin-offset": { - "x": 12.59, - "y": 14.85 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B1": { - "x": 9, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C1": { - "x": 18, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D1": { - "x": 27, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E1": { - "x": 36, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F1": { - "x": 45, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G1": { - "x": 54, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H1": { - "x": 63, - "y": 0, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A2": { - "x": 0, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B2": { - "x": 9, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C2": { - "x": 18, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D2": { - "x": 27, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E2": { - "x": 36, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F2": { - "x": 45, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G2": { - "x": 54, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H2": { - "x": 63, - "y": 9, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A3": { - "x": 0, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B3": { - "x": 9, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C3": { - "x": 18, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D3": { - "x": 27, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E3": { - "x": 36, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F3": { - "x": 45, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G3": { - "x": 54, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H3": { - "x": 63, - "y": 18, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A4": { - "x": 0, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B4": { - "x": 9, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C4": { - "x": 18, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D4": { - "x": 27, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E4": { - "x": 36, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F4": { - "x": 45, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G4": { - "x": 54, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H4": { - "x": 63, - "y": 27, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A5": { - "x": 0, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B5": { - "x": 9, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C5": { - "x": 18, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D5": { - "x": 27, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E5": { - "x": 36, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F5": { - "x": 45, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G5": { - "x": 54, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H5": { - "x": 63, - "y": 36, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A6": { - "x": 0, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B6": { - "x": 9, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C6": { - "x": 18, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D6": { - "x": 27, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E6": { - "x": 36, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F6": { - "x": 45, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G6": { - "x": 54, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H6": { - "x": 63, - "y": 45, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A7": { - "x": 0, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B7": { - "x": 9, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C7": { - "x": 18, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D7": { - "x": 27, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E7": { - "x": 36, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F7": { - "x": 45, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G7": { - "x": 54, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H7": { - "x": 63, - "y": 54, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A8": { - "x": 0, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B8": { - "x": 9, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C8": { - "x": 18, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D8": { - "x": 27, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E8": { - "x": 36, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F8": { - "x": 45, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G8": { - "x": 54, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H8": { - "x": 63, - "y": 63, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A9": { - "x": 0, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B9": { - "x": 9, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C9": { - "x": 18, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D9": { - "x": 27, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E9": { - "x": 36, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F9": { - "x": 45, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G9": { - "x": 54, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H9": { - "x": 63, - "y": 72, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A10": { - "x": 0, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B10": { - "x": 9, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C10": { - "x": 18, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D10": { - "x": 27, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E10": { - "x": 36, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F10": { - "x": 45, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G10": { - "x": 54, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H10": { - "x": 63, - "y": 81, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A11": { - "x": 0, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B11": { - "x": 9, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C11": { - "x": 18, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D11": { - "x": 27, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E11": { - "x": 36, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F11": { - "x": 45, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G11": { - "x": 54, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H11": { - "x": 63, - "y": 90, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "A12": { - "x": 0, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "B12": { - "x": 9, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "C12": { - "x": 18, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "D12": { - "x": 27, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "E12": { - "x": 36, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "F12": { - "x": 45, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "G12": { - "x": 54, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - }, - "H12": { - "x": 63, - "y": 99, - "z": 6, - "diameter": 3.5, - "depth": 60 - } - } - }, - - "tiprack-1000ul": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "diameter": 6.4, - "depth": 101.0 - } - } - }, - "opentrons-tiprack-1000ul": { - "origin-offset": { - "x": 8.5, - "y": 11.18 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "diameter": 7.62, - "depth": 98.07 - } - } - }, - "tube-rack-.75ml": { - "origin-offset": { - "x": 13.5, - "y": 15, - "z": 55 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "B1": { - "x": 19.5, - "y": 0, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "C1": { - "x": 39, - "y": 0, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "D1": { - "x": 58.5, - "y": 0, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "A2": { - "x": 0, - "y": 19.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "B2": { - "x": 19.5, - "y": 19.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "C2": { - "x": 39, - "y": 19.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "D2": { - "x": 58.5, - "y": 19.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "A3": { - "x": 0, - "y": 39, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "B3": { - "x": 19.5, - "y": 39, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "C3": { - "x": 39, - "y": 39, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "D3": { - "x": 58.5, - "y": 39, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "A4": { - "x": 0, - "y": 58.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "B4": { - "x": 19.5, - "y": 58.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "C4": { - "x": 39, - "y": 58.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "D4": { - "x": 58.5, - "y": 58.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "A5": { - "x": 0, - "y": 78, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "B5": { - "x": 19.5, - "y": 78, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "C5": { - "x": 39, - "y": 78, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "D5": { - "x": 58.5, - "y": 78, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "A6": { - "x": 0, - "y": 97.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "B6": { - "x": 19.5, - "y": 97.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "C6": { - "x": 39, - "y": 97.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - }, - "D6": { - "x": 58.5, - "y": 97.5, - "z": 0, - "depth": 20, - "diameter": 6, - "total-liquid-volume": 750 - } - } - }, - - "tube-rack-2ml": { - "origin-offset": { - "x": 13, - "y": 16, - "z": 52 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 19.5, - "y": 0, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 39, - "y": 0, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 58.5, - "y": 0, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A2": { - "x": 0, - "y": 19.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 19.5, - "y": 19.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 39, - "y": 19.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 58.5, - "y": 19.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A3": { - "x": 0, - "y": 39, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 19.5, - "y": 39, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 39, - "y": 39, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 58.5, - "y": 39, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A4": { - "x": 0, - "y": 58.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 19.5, - "y": 58.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 39, - "y": 58.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 58.5, - "y": 58.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A5": { - "x": 0, - "y": 78, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 19.5, - "y": 78, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 39, - "y": 78, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 58.5, - "y": 78, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "A6": { - "x": 0, - "y": 97.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 19.5, - "y": 97.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 39, - "y": 97.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 58.5, - "y": 97.5, - "z": 0, - "depth": 40, - "diameter": 6, - "total-liquid-volume": 2000 - } - } - }, - - "tube-rack-15_50ml": { - "origin-offset": { - "x": 11, - "y": 19 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 5, - "depth": 115, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "B1": { - "x": 31.5, - "y": 0, - "z": 5, - "depth": 115, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "C1": { - "x": 63, - "y": 0, - "z": 5, - "depth": 115, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "A2": { - "x": 0, - "y": 22.7, - "z": 5, - "depth": 115, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "B2": { - "x": 31.5, - "y": 22.7, - "z": 5, - "depth": 115, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "C2": { - "x": 63, - "y": 22.7, - "z": 0, - "depth": 115, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "A3": { - "x": 5.9, - "y": 51.26, - "z": 5, - "depth": 115, - "diameter": 30, - "total-liquid-volume": 50000 - }, - "B3": { - "x": 51.26, - "y": 51.26, - "z": 5, - "depth": 115, - "diameter": 30, - "total-liquid-volume": 50000 - }, - "A4": { - "x": 5.9, - "y": 87.1, - "z": 5, - "depth": 115, - "diameter": 30, - "total-liquid-volume": 50000 - }, - "B4": { - "x": 51.26, - "y": 87.1, - "z": 5, - "depth": 115, - "diameter": 30, - "total-liquid-volume": 50000 - } - } - }, - - "trough-12row": { - "origin-offset": { - "x": 7.75, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A2": { - "x": 0, - "y": 9, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A3": { - "x": 0, - "y": 18, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A4": { - "x": 0, - "y": 27, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A5": { - "x": 0, - "y": 36, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A6": { - "x": 0, - "y": 45, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A7": { - "x": 0, - "y": 54, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A8": { - "x": 0, - "y": 63, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A9": { - "x": 0, - "y": 72, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A10": { - "x": 0, - "y": 81, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A11": { - "x": 0, - "y": 90, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - }, - "A12": { - "x": 0, - "y": 99, - "z": 8, - "depth": 38, - "total-liquid-volume": 22000 - } - } - }, - - "trough-12row-short": { - "origin-offset": { - "x": 7.75, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 20, - "total-liquid-volume": 22000 - } - } - }, - - "24-vial-rack": { - "origin-offset": { - "x": 13.67, - "y": 16 - }, - "locations": { - "A1": { - "x": 0.0, - "total-liquid-volume": 3400, - "y": 0.0, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "A2": { - "x": 0.0, - "total-liquid-volume": 3400, - "y": 19.3, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "A3": { - "x": 0.0, - "total-liquid-volume": 3400, - "y": 38.6, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "A4": { - "x": 0.0, - "total-liquid-volume": 3400, - "y": 57.9, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "A5": { - "x": 0.0, - "total-liquid-volume": 3400, - "y": 77.2, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "A6": { - "x": 0.0, - "total-liquid-volume": 3400, - "y": 96.5, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "B1": { - "x": 19.3, - "total-liquid-volume": 3400, - "y": 0.0, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "B2": { - "x": 19.3, - "total-liquid-volume": 3400, - "y": 19.3, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "B3": { - "x": 19.3, - "total-liquid-volume": 3400, - "y": 38.6, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "B4": { - "x": 19.3, - "total-liquid-volume": 3400, - "y": 57.9, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "B5": { - "x": 19.3, - "total-liquid-volume": 3400, - "y": 77.2, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "B6": { - "x": 19.3, - "total-liquid-volume": 3400, - "y": 96.5, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "C1": { - "x": 38.6, - "total-liquid-volume": 3400, - "y": 0.0, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "C2": { - "x": 38.6, - "total-liquid-volume": 3400, - "y": 19.3, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "C3": { - "x": 38.6, - "total-liquid-volume": 3400, - "y": 38.6, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "C4": { - "x": 38.6, - "total-liquid-volume": 3400, - "y": 57.9, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "C5": { - "x": 38.6, - "total-liquid-volume": 3400, - "y": 77.2, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "C6": { - "x": 38.6, - "total-liquid-volume": 3400, - "y": 96.5, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "D1": { - "x": 57.9, - "total-liquid-volume": 3400, - "y": 0.0, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "D2": { - "x": 57.9, - "total-liquid-volume": 3400, - "y": 19.3, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "D3": { - "x": 57.9, - "total-liquid-volume": 3400, - "y": 38.6, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "D4": { - "x": 57.9, - "total-liquid-volume": 3400, - "y": 57.9, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "D5": { - "x": 57.9, - "total-liquid-volume": 3400, - "y": 77.2, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - }, - "D6": { - "x": 57.9, - "total-liquid-volume": 3400, - "y": 96.5, - "depth": 16.2, - "z": 0, - "diameter": 15.62 - } - } - }, - - "96-deep-well": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 33.5, - "diameter": 7.5, - "total-liquid-volume": 2000 - } - } - }, - - "96-PCR-tall": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 15.4, - "diameter": 6.4, - "total-liquid-volume": 300 - } - } - }, - - "96-PCR-flat": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 300 - } - } - }, - - "biorad-hardshell-96-PCR": { - "origin-offset": { - "x": 18.24, - "y": 13.63 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B1": { - "x": 9, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C1": { - "x": 18, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D1": { - "x": 27, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E1": { - "x": 36, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F1": { - "x": 45, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G1": { - "x": 54, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H1": { - "x": 63, - "y": 0, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A2": { - "x": 0, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B2": { - "x": 9, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C2": { - "x": 18, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D2": { - "x": 27, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E2": { - "x": 36, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F2": { - "x": 45, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G2": { - "x": 54, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H2": { - "x": 63, - "y": 9, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A3": { - "x": 0, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B3": { - "x": 9, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C3": { - "x": 18, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D3": { - "x": 27, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E3": { - "x": 36, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F3": { - "x": 45, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G3": { - "x": 54, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H3": { - "x": 63, - "y": 18, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A4": { - "x": 0, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B4": { - "x": 9, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C4": { - "x": 18, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D4": { - "x": 27, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E4": { - "x": 36, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F4": { - "x": 45, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G4": { - "x": 54, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H4": { - "x": 63, - "y": 27, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A5": { - "x": 0, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B5": { - "x": 9, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C5": { - "x": 18, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D5": { - "x": 27, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E5": { - "x": 36, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F5": { - "x": 45, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G5": { - "x": 54, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H5": { - "x": 63, - "y": 36, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A6": { - "x": 0, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B6": { - "x": 9, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C6": { - "x": 18, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D6": { - "x": 27, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E6": { - "x": 36, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F6": { - "x": 45, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G6": { - "x": 54, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H6": { - "x": 63, - "y": 45, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A7": { - "x": 0, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B7": { - "x": 9, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C7": { - "x": 18, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D7": { - "x": 27, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E7": { - "x": 36, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F7": { - "x": 45, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G7": { - "x": 54, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H7": { - "x": 63, - "y": 54, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A8": { - "x": 0, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B8": { - "x": 9, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C8": { - "x": 18, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D8": { - "x": 27, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E8": { - "x": 36, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F8": { - "x": 45, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G8": { - "x": 54, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H8": { - "x": 63, - "y": 63, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A9": { - "x": 0, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B9": { - "x": 9, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C9": { - "x": 18, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D9": { - "x": 27, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E9": { - "x": 36, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F9": { - "x": 45, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G9": { - "x": 54, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H9": { - "x": 63, - "y": 72, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A10": { - "x": 0, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B10": { - "x": 9, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C10": { - "x": 18, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D10": { - "x": 27, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E10": { - "x": 36, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F10": { - "x": 45, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G10": { - "x": 54, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H10": { - "x": 63, - "y": 81, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A11": { - "x": 0, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B11": { - "x": 9, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C11": { - "x": 18, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D11": { - "x": 27, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E11": { - "x": 36, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F11": { - "x": 45, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G11": { - "x": 54, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H11": { - "x": 63, - "y": 90, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A12": { - "x": 0, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B12": { - "x": 9, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C12": { - "x": 18, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D12": { - "x": 27, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E12": { - "x": 36, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F12": { - "x": 45, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G12": { - "x": 54, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H12": { - "x": 63, - "y": 99, - "z": 4.25, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 300 - } - } - }, - - "96-flat": { - "origin-offset": { - "x": 17.64, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B1": { - "x": 9, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C1": { - "x": 18, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D1": { - "x": 27, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E1": { - "x": 36, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F1": { - "x": 45, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G1": { - "x": 54, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H1": { - "x": 63, - "y": 0, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A2": { - "x": 0, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B2": { - "x": 9, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C2": { - "x": 18, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D2": { - "x": 27, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E2": { - "x": 36, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F2": { - "x": 45, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G2": { - "x": 54, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H2": { - "x": 63, - "y": 9, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A3": { - "x": 0, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B3": { - "x": 9, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C3": { - "x": 18, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D3": { - "x": 27, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E3": { - "x": 36, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F3": { - "x": 45, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G3": { - "x": 54, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H3": { - "x": 63, - "y": 18, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A4": { - "x": 0, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B4": { - "x": 9, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C4": { - "x": 18, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D4": { - "x": 27, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E4": { - "x": 36, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F4": { - "x": 45, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G4": { - "x": 54, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H4": { - "x": 63, - "y": 27, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A5": { - "x": 0, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B5": { - "x": 9, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C5": { - "x": 18, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D5": { - "x": 27, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E5": { - "x": 36, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F5": { - "x": 45, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G5": { - "x": 54, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H5": { - "x": 63, - "y": 36, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A6": { - "x": 0, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B6": { - "x": 9, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C6": { - "x": 18, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D6": { - "x": 27, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E6": { - "x": 36, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F6": { - "x": 45, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G6": { - "x": 54, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H6": { - "x": 63, - "y": 45, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A7": { - "x": 0, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B7": { - "x": 9, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C7": { - "x": 18, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D7": { - "x": 27, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E7": { - "x": 36, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F7": { - "x": 45, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G7": { - "x": 54, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H7": { - "x": 63, - "y": 54, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A8": { - "x": 0, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B8": { - "x": 9, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C8": { - "x": 18, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D8": { - "x": 27, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E8": { - "x": 36, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F8": { - "x": 45, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G8": { - "x": 54, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H8": { - "x": 63, - "y": 63, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A9": { - "x": 0, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B9": { - "x": 9, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C9": { - "x": 18, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D9": { - "x": 27, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E9": { - "x": 36, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F9": { - "x": 45, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G9": { - "x": 54, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H9": { - "x": 63, - "y": 72, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A10": { - "x": 0, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B10": { - "x": 9, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C10": { - "x": 18, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D10": { - "x": 27, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E10": { - "x": 36, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F10": { - "x": 45, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G10": { - "x": 54, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H10": { - "x": 63, - "y": 81, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A11": { - "x": 0, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B11": { - "x": 9, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C11": { - "x": 18, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D11": { - "x": 27, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E11": { - "x": 36, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F11": { - "x": 45, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G11": { - "x": 54, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H11": { - "x": 63, - "y": 90, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "A12": { - "x": 0, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "B12": { - "x": 9, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "C12": { - "x": 18, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "D12": { - "x": 27, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "E12": { - "x": 36, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "F12": { - "x": 45, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "G12": { - "x": 54, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - }, - "H12": { - "x": 63, - "y": 99, - "z": 3.85, - "depth": 10.5, - "diameter": 6.4, - "total-liquid-volume": 400 - } - } - }, - - "PCR-strip-tall": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 19.5, - "diameter": 6.4, - "total-liquid-volume": 280 - } - } - }, - - "384-plate": { - "origin-offset": { - "x": 9, - "y": 12.13 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B1": { - "x": 4.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D1": { - "x": 13.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F1": { - "x": 22.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H1": { - "x": 31.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J1": { - "x": 40.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L1": { - "x": 49.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N1": { - "x": 58.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P1": { - "x": 67.5, - "y": 0, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A2": { - "x": 0, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B2": { - "x": 4.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C2": { - "x": 9, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D2": { - "x": 13.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E2": { - "x": 18, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F2": { - "x": 22.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G2": { - "x": 27, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H2": { - "x": 31.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I2": { - "x": 36, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J2": { - "x": 40.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K2": { - "x": 45, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L2": { - "x": 49.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M2": { - "x": 54, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N2": { - "x": 58.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O2": { - "x": 63, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P2": { - "x": 67.5, - "y": 4.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A3": { - "x": 0, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B3": { - "x": 4.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C3": { - "x": 9, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D3": { - "x": 13.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E3": { - "x": 18, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F3": { - "x": 22.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G3": { - "x": 27, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H3": { - "x": 31.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I3": { - "x": 36, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J3": { - "x": 40.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K3": { - "x": 45, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L3": { - "x": 49.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M3": { - "x": 54, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N3": { - "x": 58.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O3": { - "x": 63, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P3": { - "x": 67.5, - "y": 9, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A4": { - "x": 0, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B4": { - "x": 4.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C4": { - "x": 9, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D4": { - "x": 13.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E4": { - "x": 18, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F4": { - "x": 22.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G4": { - "x": 27, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H4": { - "x": 31.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I4": { - "x": 36, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J4": { - "x": 40.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K4": { - "x": 45, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L4": { - "x": 49.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M4": { - "x": 54, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N4": { - "x": 58.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O4": { - "x": 63, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P4": { - "x": 67.5, - "y": 13.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A5": { - "x": 0, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B5": { - "x": 4.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C5": { - "x": 9, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D5": { - "x": 13.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E5": { - "x": 18, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F5": { - "x": 22.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G5": { - "x": 27, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H5": { - "x": 31.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I5": { - "x": 36, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J5": { - "x": 40.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K5": { - "x": 45, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L5": { - "x": 49.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M5": { - "x": 54, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N5": { - "x": 58.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O5": { - "x": 63, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P5": { - "x": 67.5, - "y": 18, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A6": { - "x": 0, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B6": { - "x": 4.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C6": { - "x": 9, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D6": { - "x": 13.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E6": { - "x": 18, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F6": { - "x": 22.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G6": { - "x": 27, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H6": { - "x": 31.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I6": { - "x": 36, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J6": { - "x": 40.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K6": { - "x": 45, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L6": { - "x": 49.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M6": { - "x": 54, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N6": { - "x": 58.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O6": { - "x": 63, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P6": { - "x": 67.5, - "y": 22.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A7": { - "x": 0, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B7": { - "x": 4.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C7": { - "x": 9, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D7": { - "x": 13.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E7": { - "x": 18, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F7": { - "x": 22.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G7": { - "x": 27, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H7": { - "x": 31.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I7": { - "x": 36, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J7": { - "x": 40.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K7": { - "x": 45, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L7": { - "x": 49.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M7": { - "x": 54, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N7": { - "x": 58.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O7": { - "x": 63, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P7": { - "x": 67.5, - "y": 27, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A8": { - "x": 0, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B8": { - "x": 4.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C8": { - "x": 9, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D8": { - "x": 13.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E8": { - "x": 18, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F8": { - "x": 22.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G8": { - "x": 27, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H8": { - "x": 31.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I8": { - "x": 36, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J8": { - "x": 40.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K8": { - "x": 45, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L8": { - "x": 49.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M8": { - "x": 54, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N8": { - "x": 58.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O8": { - "x": 63, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P8": { - "x": 67.5, - "y": 31.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A9": { - "x": 0, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B9": { - "x": 4.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C9": { - "x": 9, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D9": { - "x": 13.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E9": { - "x": 18, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F9": { - "x": 22.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G9": { - "x": 27, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H9": { - "x": 31.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I9": { - "x": 36, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J9": { - "x": 40.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K9": { - "x": 45, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L9": { - "x": 49.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M9": { - "x": 54, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N9": { - "x": 58.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O9": { - "x": 63, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P9": { - "x": 67.5, - "y": 36, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A10": { - "x": 0, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B10": { - "x": 4.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C10": { - "x": 9, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D10": { - "x": 13.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E10": { - "x": 18, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F10": { - "x": 22.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G10": { - "x": 27, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H10": { - "x": 31.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I10": { - "x": 36, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J10": { - "x": 40.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K10": { - "x": 45, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L10": { - "x": 49.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M10": { - "x": 54, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N10": { - "x": 58.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O10": { - "x": 63, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P10": { - "x": 67.5, - "y": 40.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A11": { - "x": 0, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B11": { - "x": 4.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C11": { - "x": 9, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D11": { - "x": 13.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E11": { - "x": 18, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F11": { - "x": 22.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G11": { - "x": 27, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H11": { - "x": 31.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I11": { - "x": 36, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J11": { - "x": 40.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K11": { - "x": 45, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L11": { - "x": 49.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M11": { - "x": 54, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N11": { - "x": 58.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O11": { - "x": 63, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P11": { - "x": 67.5, - "y": 45, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A12": { - "x": 0, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B12": { - "x": 4.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C12": { - "x": 9, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D12": { - "x": 13.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E12": { - "x": 18, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F12": { - "x": 22.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G12": { - "x": 27, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H12": { - "x": 31.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I12": { - "x": 36, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J12": { - "x": 40.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K12": { - "x": 45, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L12": { - "x": 49.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M12": { - "x": 54, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N12": { - "x": 58.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O12": { - "x": 63, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P12": { - "x": 67.5, - "y": 49.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A13": { - "x": 0, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B13": { - "x": 4.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C13": { - "x": 9, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D13": { - "x": 13.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E13": { - "x": 18, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F13": { - "x": 22.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G13": { - "x": 27, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H13": { - "x": 31.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I13": { - "x": 36, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J13": { - "x": 40.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K13": { - "x": 45, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L13": { - "x": 49.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M13": { - "x": 54, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N13": { - "x": 58.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O13": { - "x": 63, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P13": { - "x": 67.5, - "y": 54, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A14": { - "x": 0, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B14": { - "x": 4.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C14": { - "x": 9, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D14": { - "x": 13.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E14": { - "x": 18, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F14": { - "x": 22.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G14": { - "x": 27, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H14": { - "x": 31.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I14": { - "x": 36, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J14": { - "x": 40.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K14": { - "x": 45, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L14": { - "x": 49.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M14": { - "x": 54, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N14": { - "x": 58.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O14": { - "x": 63, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P14": { - "x": 67.5, - "y": 58.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A15": { - "x": 0, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B15": { - "x": 4.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C15": { - "x": 9, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D15": { - "x": 13.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E15": { - "x": 18, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F15": { - "x": 22.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G15": { - "x": 27, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H15": { - "x": 31.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I15": { - "x": 36, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J15": { - "x": 40.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K15": { - "x": 45, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L15": { - "x": 49.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M15": { - "x": 54, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N15": { - "x": 58.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O15": { - "x": 63, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P15": { - "x": 67.5, - "y": 63, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A16": { - "x": 0, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B16": { - "x": 4.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C16": { - "x": 9, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D16": { - "x": 13.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E16": { - "x": 18, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F16": { - "x": 22.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G16": { - "x": 27, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H16": { - "x": 31.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I16": { - "x": 36, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J16": { - "x": 40.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K16": { - "x": 45, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L16": { - "x": 49.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M16": { - "x": 54, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N16": { - "x": 58.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O16": { - "x": 63, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P16": { - "x": 67.5, - "y": 67.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A17": { - "x": 0, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B17": { - "x": 4.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C17": { - "x": 9, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D17": { - "x": 13.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E17": { - "x": 18, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F17": { - "x": 22.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G17": { - "x": 27, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H17": { - "x": 31.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I17": { - "x": 36, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J17": { - "x": 40.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K17": { - "x": 45, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L17": { - "x": 49.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M17": { - "x": 54, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N17": { - "x": 58.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O17": { - "x": 63, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P17": { - "x": 67.5, - "y": 72, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A18": { - "x": 0, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B18": { - "x": 4.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C18": { - "x": 9, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D18": { - "x": 13.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E18": { - "x": 18, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F18": { - "x": 22.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G18": { - "x": 27, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H18": { - "x": 31.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I18": { - "x": 36, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J18": { - "x": 40.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K18": { - "x": 45, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L18": { - "x": 49.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M18": { - "x": 54, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N18": { - "x": 58.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O18": { - "x": 63, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P18": { - "x": 67.5, - "y": 76.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A19": { - "x": 0, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B19": { - "x": 4.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C19": { - "x": 9, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D19": { - "x": 13.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E19": { - "x": 18, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F19": { - "x": 22.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G19": { - "x": 27, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H19": { - "x": 31.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I19": { - "x": 36, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J19": { - "x": 40.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K19": { - "x": 45, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L19": { - "x": 49.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M19": { - "x": 54, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N19": { - "x": 58.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O19": { - "x": 63, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P19": { - "x": 67.5, - "y": 81, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A20": { - "x": 0, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B20": { - "x": 4.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C20": { - "x": 9, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D20": { - "x": 13.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E20": { - "x": 18, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F20": { - "x": 22.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G20": { - "x": 27, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H20": { - "x": 31.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I20": { - "x": 36, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J20": { - "x": 40.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K20": { - "x": 45, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L20": { - "x": 49.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M20": { - "x": 54, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N20": { - "x": 58.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O20": { - "x": 63, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P20": { - "x": 67.5, - "y": 85.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A21": { - "x": 0, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B21": { - "x": 4.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C21": { - "x": 9, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D21": { - "x": 13.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E21": { - "x": 18, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F21": { - "x": 22.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G21": { - "x": 27, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H21": { - "x": 31.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I21": { - "x": 36, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J21": { - "x": 40.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K21": { - "x": 45, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L21": { - "x": 49.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M21": { - "x": 54, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N21": { - "x": 58.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O21": { - "x": 63, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P21": { - "x": 67.5, - "y": 90, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A22": { - "x": 0, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B22": { - "x": 4.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C22": { - "x": 9, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D22": { - "x": 13.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E22": { - "x": 18, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F22": { - "x": 22.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G22": { - "x": 27, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H22": { - "x": 31.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I22": { - "x": 36, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J22": { - "x": 40.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K22": { - "x": 45, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L22": { - "x": 49.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M22": { - "x": 54, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N22": { - "x": 58.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O22": { - "x": 63, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P22": { - "x": 67.5, - "y": 94.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A23": { - "x": 0, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B23": { - "x": 4.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C23": { - "x": 9, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D23": { - "x": 13.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E23": { - "x": 18, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F23": { - "x": 22.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G23": { - "x": 27, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H23": { - "x": 31.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I23": { - "x": 36, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J23": { - "x": 40.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K23": { - "x": 45, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L23": { - "x": 49.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M23": { - "x": 54, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N23": { - "x": 58.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O23": { - "x": 63, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P23": { - "x": 67.5, - "y": 99, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A24": { - "x": 0, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B24": { - "x": 4.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C24": { - "x": 9, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D24": { - "x": 13.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E24": { - "x": 18, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F24": { - "x": 22.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G24": { - "x": 27, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H24": { - "x": 31.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I24": { - "x": 36, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J24": { - "x": 40.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K24": { - "x": 45, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L24": { - "x": 49.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M24": { - "x": 54, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N24": { - "x": 58.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O24": { - "x": 63, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P24": { - "x": 67.5, - "y": 103.5, - "z": 0, - "depth": 9.5, - "diameter": 3.1, - "total-liquid-volume": 55 - } - } - }, - - "MALDI-plate": { - "origin-offset": { - "x": 9, - "y": 12 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B1": { - "x": 4.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D1": { - "x": 13.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F1": { - "x": 22.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H1": { - "x": 31.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J1": { - "x": 40.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L1": { - "x": 49.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N1": { - "x": 58.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P1": { - "x": 67.5, - "y": 0, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A2": { - "x": 0, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B2": { - "x": 4.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C2": { - "x": 9, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D2": { - "x": 13.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E2": { - "x": 18, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F2": { - "x": 22.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G2": { - "x": 27, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H2": { - "x": 31.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I2": { - "x": 36, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J2": { - "x": 40.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K2": { - "x": 45, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L2": { - "x": 49.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M2": { - "x": 54, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N2": { - "x": 58.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O2": { - "x": 63, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P2": { - "x": 67.5, - "y": 4.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A3": { - "x": 0, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B3": { - "x": 4.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C3": { - "x": 9, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D3": { - "x": 13.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E3": { - "x": 18, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F3": { - "x": 22.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G3": { - "x": 27, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H3": { - "x": 31.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I3": { - "x": 36, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J3": { - "x": 40.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K3": { - "x": 45, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L3": { - "x": 49.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M3": { - "x": 54, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N3": { - "x": 58.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O3": { - "x": 63, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P3": { - "x": 67.5, - "y": 9, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A4": { - "x": 0, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B4": { - "x": 4.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C4": { - "x": 9, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D4": { - "x": 13.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E4": { - "x": 18, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F4": { - "x": 22.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G4": { - "x": 27, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H4": { - "x": 31.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I4": { - "x": 36, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J4": { - "x": 40.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K4": { - "x": 45, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L4": { - "x": 49.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M4": { - "x": 54, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N4": { - "x": 58.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O4": { - "x": 63, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P4": { - "x": 67.5, - "y": 13.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A5": { - "x": 0, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B5": { - "x": 4.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C5": { - "x": 9, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D5": { - "x": 13.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E5": { - "x": 18, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F5": { - "x": 22.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G5": { - "x": 27, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H5": { - "x": 31.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I5": { - "x": 36, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J5": { - "x": 40.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K5": { - "x": 45, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L5": { - "x": 49.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M5": { - "x": 54, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N5": { - "x": 58.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O5": { - "x": 63, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P5": { - "x": 67.5, - "y": 18, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A6": { - "x": 0, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B6": { - "x": 4.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C6": { - "x": 9, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D6": { - "x": 13.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E6": { - "x": 18, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F6": { - "x": 22.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G6": { - "x": 27, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H6": { - "x": 31.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I6": { - "x": 36, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J6": { - "x": 40.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K6": { - "x": 45, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L6": { - "x": 49.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M6": { - "x": 54, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N6": { - "x": 58.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O6": { - "x": 63, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P6": { - "x": 67.5, - "y": 22.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A7": { - "x": 0, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B7": { - "x": 4.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C7": { - "x": 9, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D7": { - "x": 13.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E7": { - "x": 18, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F7": { - "x": 22.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G7": { - "x": 27, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H7": { - "x": 31.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I7": { - "x": 36, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J7": { - "x": 40.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K7": { - "x": 45, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L7": { - "x": 49.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M7": { - "x": 54, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N7": { - "x": 58.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O7": { - "x": 63, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P7": { - "x": 67.5, - "y": 27, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A8": { - "x": 0, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B8": { - "x": 4.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C8": { - "x": 9, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D8": { - "x": 13.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E8": { - "x": 18, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F8": { - "x": 22.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G8": { - "x": 27, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H8": { - "x": 31.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I8": { - "x": 36, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J8": { - "x": 40.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K8": { - "x": 45, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L8": { - "x": 49.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M8": { - "x": 54, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N8": { - "x": 58.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O8": { - "x": 63, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P8": { - "x": 67.5, - "y": 31.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A9": { - "x": 0, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B9": { - "x": 4.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C9": { - "x": 9, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D9": { - "x": 13.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E9": { - "x": 18, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F9": { - "x": 22.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G9": { - "x": 27, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H9": { - "x": 31.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I9": { - "x": 36, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J9": { - "x": 40.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K9": { - "x": 45, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L9": { - "x": 49.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M9": { - "x": 54, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N9": { - "x": 58.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O9": { - "x": 63, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P9": { - "x": 67.5, - "y": 36, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A10": { - "x": 0, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B10": { - "x": 4.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C10": { - "x": 9, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D10": { - "x": 13.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E10": { - "x": 18, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F10": { - "x": 22.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G10": { - "x": 27, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H10": { - "x": 31.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I10": { - "x": 36, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J10": { - "x": 40.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K10": { - "x": 45, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L10": { - "x": 49.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M10": { - "x": 54, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N10": { - "x": 58.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O10": { - "x": 63, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P10": { - "x": 67.5, - "y": 40.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A11": { - "x": 0, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B11": { - "x": 4.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C11": { - "x": 9, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D11": { - "x": 13.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E11": { - "x": 18, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F11": { - "x": 22.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G11": { - "x": 27, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H11": { - "x": 31.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I11": { - "x": 36, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J11": { - "x": 40.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K11": { - "x": 45, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L11": { - "x": 49.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M11": { - "x": 54, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N11": { - "x": 58.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O11": { - "x": 63, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P11": { - "x": 67.5, - "y": 45, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A12": { - "x": 0, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B12": { - "x": 4.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C12": { - "x": 9, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D12": { - "x": 13.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E12": { - "x": 18, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F12": { - "x": 22.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G12": { - "x": 27, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H12": { - "x": 31.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I12": { - "x": 36, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J12": { - "x": 40.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K12": { - "x": 45, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L12": { - "x": 49.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M12": { - "x": 54, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N12": { - "x": 58.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O12": { - "x": 63, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P12": { - "x": 67.5, - "y": 49.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A13": { - "x": 0, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B13": { - "x": 4.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C13": { - "x": 9, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D13": { - "x": 13.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E13": { - "x": 18, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F13": { - "x": 22.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G13": { - "x": 27, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H13": { - "x": 31.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I13": { - "x": 36, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J13": { - "x": 40.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K13": { - "x": 45, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L13": { - "x": 49.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M13": { - "x": 54, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N13": { - "x": 58.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O13": { - "x": 63, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P13": { - "x": 67.5, - "y": 54, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A14": { - "x": 0, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B14": { - "x": 4.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C14": { - "x": 9, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D14": { - "x": 13.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E14": { - "x": 18, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F14": { - "x": 22.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G14": { - "x": 27, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H14": { - "x": 31.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I14": { - "x": 36, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J14": { - "x": 40.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K14": { - "x": 45, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L14": { - "x": 49.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M14": { - "x": 54, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N14": { - "x": 58.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O14": { - "x": 63, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P14": { - "x": 67.5, - "y": 58.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A15": { - "x": 0, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B15": { - "x": 4.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C15": { - "x": 9, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D15": { - "x": 13.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E15": { - "x": 18, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F15": { - "x": 22.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G15": { - "x": 27, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H15": { - "x": 31.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I15": { - "x": 36, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J15": { - "x": 40.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K15": { - "x": 45, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L15": { - "x": 49.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M15": { - "x": 54, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N15": { - "x": 58.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O15": { - "x": 63, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P15": { - "x": 67.5, - "y": 63, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A16": { - "x": 0, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B16": { - "x": 4.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C16": { - "x": 9, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D16": { - "x": 13.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E16": { - "x": 18, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F16": { - "x": 22.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G16": { - "x": 27, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H16": { - "x": 31.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I16": { - "x": 36, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J16": { - "x": 40.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K16": { - "x": 45, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L16": { - "x": 49.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M16": { - "x": 54, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N16": { - "x": 58.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O16": { - "x": 63, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P16": { - "x": 67.5, - "y": 67.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A17": { - "x": 0, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B17": { - "x": 4.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C17": { - "x": 9, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D17": { - "x": 13.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E17": { - "x": 18, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F17": { - "x": 22.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G17": { - "x": 27, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H17": { - "x": 31.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I17": { - "x": 36, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J17": { - "x": 40.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K17": { - "x": 45, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L17": { - "x": 49.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M17": { - "x": 54, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N17": { - "x": 58.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O17": { - "x": 63, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P17": { - "x": 67.5, - "y": 72, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A18": { - "x": 0, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B18": { - "x": 4.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C18": { - "x": 9, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D18": { - "x": 13.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E18": { - "x": 18, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F18": { - "x": 22.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G18": { - "x": 27, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H18": { - "x": 31.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I18": { - "x": 36, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J18": { - "x": 40.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K18": { - "x": 45, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L18": { - "x": 49.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M18": { - "x": 54, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N18": { - "x": 58.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O18": { - "x": 63, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P18": { - "x": 67.5, - "y": 76.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A19": { - "x": 0, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B19": { - "x": 4.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C19": { - "x": 9, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D19": { - "x": 13.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E19": { - "x": 18, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F19": { - "x": 22.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G19": { - "x": 27, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H19": { - "x": 31.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I19": { - "x": 36, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J19": { - "x": 40.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K19": { - "x": 45, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L19": { - "x": 49.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M19": { - "x": 54, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N19": { - "x": 58.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O19": { - "x": 63, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P19": { - "x": 67.5, - "y": 81, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A20": { - "x": 0, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B20": { - "x": 4.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C20": { - "x": 9, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D20": { - "x": 13.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E20": { - "x": 18, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F20": { - "x": 22.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G20": { - "x": 27, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H20": { - "x": 31.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I20": { - "x": 36, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J20": { - "x": 40.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K20": { - "x": 45, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L20": { - "x": 49.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M20": { - "x": 54, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N20": { - "x": 58.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O20": { - "x": 63, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P20": { - "x": 67.5, - "y": 85.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A21": { - "x": 0, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B21": { - "x": 4.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C21": { - "x": 9, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D21": { - "x": 13.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E21": { - "x": 18, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F21": { - "x": 22.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G21": { - "x": 27, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H21": { - "x": 31.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I21": { - "x": 36, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J21": { - "x": 40.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K21": { - "x": 45, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L21": { - "x": 49.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M21": { - "x": 54, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N21": { - "x": 58.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O21": { - "x": 63, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P21": { - "x": 67.5, - "y": 90, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A22": { - "x": 0, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B22": { - "x": 4.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C22": { - "x": 9, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D22": { - "x": 13.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E22": { - "x": 18, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F22": { - "x": 22.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G22": { - "x": 27, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H22": { - "x": 31.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I22": { - "x": 36, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J22": { - "x": 40.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K22": { - "x": 45, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L22": { - "x": 49.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M22": { - "x": 54, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N22": { - "x": 58.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O22": { - "x": 63, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P22": { - "x": 67.5, - "y": 94.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A23": { - "x": 0, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B23": { - "x": 4.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C23": { - "x": 9, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D23": { - "x": 13.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E23": { - "x": 18, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F23": { - "x": 22.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G23": { - "x": 27, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H23": { - "x": 31.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I23": { - "x": 36, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J23": { - "x": 40.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K23": { - "x": 45, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L23": { - "x": 49.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M23": { - "x": 54, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N23": { - "x": 58.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O23": { - "x": 63, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P23": { - "x": 67.5, - "y": 99, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "A24": { - "x": 0, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "B24": { - "x": 4.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "C24": { - "x": 9, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "D24": { - "x": 13.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "E24": { - "x": 18, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "F24": { - "x": 22.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "G24": { - "x": 27, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "H24": { - "x": 31.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "I24": { - "x": 36, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "J24": { - "x": 40.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "K24": { - "x": 45, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "L24": { - "x": 49.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "M24": { - "x": 54, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "N24": { - "x": 58.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "O24": { - "x": 63, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - }, - "P24": { - "x": 67.5, - "y": 103.5, - "z": 0, - "depth": 0, - "diameter": 3.1, - "total-liquid-volume": 55 - } - } - }, - - "48-vial-plate": { - "origin-offset": { - "x": 10.5, - "y": 18 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 13, - "y": 0, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 26, - "y": 0, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 39, - "y": 0, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E1": { - "x": 52, - "y": 0, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F1": { - "x": 65, - "y": 0, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - - "A2": { - "x": 0, - "y": 13, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 13, - "y": 13, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 26, - "y": 13, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 39, - "y": 13, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E2": { - "x": 52, - "y": 13, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F2": { - "x": 65, - "y": 13, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - - "A3": { - "x": 0, - "y": 26, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 13, - "y": 26, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 26, - "y": 26, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 39, - "y": 26, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E3": { - "x": 52, - "y": 26, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F3": { - "x": 65, - "y": 26, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - - "A4": { - "x": 0, - "y": 39, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 13, - "y": 39, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 26, - "y": 39, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 39, - "y": 39, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E4": { - "x": 52, - "y": 39, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F4": { - "x": 65, - "y": 39, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - - "A5": { - "x": 0, - "y": 52, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 13, - "y": 52, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 26, - "y": 52, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 39, - "y": 52, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E5": { - "x": 52, - "y": 52, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F5": { - "x": 65, - "y": 52, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - - "A6": { - "x": 0, - "y": 65, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 13, - "y": 65, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 26, - "y": 65, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 39, - "y": 65, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E6": { - "x": 52, - "y": 65, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F6": { - "x": 65, - "y": 65, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - - "A7": { - "x": 0, - "y": 78, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B7": { - "x": 13, - "y": 78, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C7": { - "x": 26, - "y": 78, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D7": { - "x": 39, - "y": 78, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E7": { - "x": 52, - "y": 78, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F7": { - "x": 65, - "y": 78, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - - "A8": { - "x": 0, - "y": 91, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "B8": { - "x": 13, - "y": 91, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "C8": { - "x": 26, - "y": 91, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "D8": { - "x": 39, - "y": 91, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "E8": { - "x": 52, - "y": 91, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - }, - "F8": { - "x": 65, - "y": 91, - "z": 0, - "depth": 30, - "diameter": 11.5, - "total-liquid-volume": 2000 - } - } - }, - - "e-gelgol": { - "origin-offset": { - "x": 11.24, - "y": 14.34 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B1": { - "x": 9, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C1": { - "x": 18, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D1": { - "x": 27, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E1": { - "x": 36, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F1": { - "x": 45, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G1": { - "x": 54, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H1": { - "x": 63, - "y": 0, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A2": { - "x": 0, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B2": { - "x": 9, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C2": { - "x": 18, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D2": { - "x": 27, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E2": { - "x": 36, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F2": { - "x": 45, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G2": { - "x": 54, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H2": { - "x": 63, - "y": 9, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A3": { - "x": 0, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B3": { - "x": 9, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C3": { - "x": 18, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D3": { - "x": 27, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E3": { - "x": 36, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F3": { - "x": 45, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G3": { - "x": 54, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H3": { - "x": 63, - "y": 18, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A4": { - "x": 0, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B4": { - "x": 9, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C4": { - "x": 18, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D4": { - "x": 27, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E4": { - "x": 36, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F4": { - "x": 45, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G4": { - "x": 54, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H4": { - "x": 63, - "y": 27, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A5": { - "x": 0, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B5": { - "x": 9, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C5": { - "x": 18, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D5": { - "x": 27, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E5": { - "x": 36, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F5": { - "x": 45, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G5": { - "x": 54, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H5": { - "x": 63, - "y": 36, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A6": { - "x": 0, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B6": { - "x": 9, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C6": { - "x": 18, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D6": { - "x": 27, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E6": { - "x": 36, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F6": { - "x": 45, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G6": { - "x": 54, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H6": { - "x": 63, - "y": 45, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A7": { - "x": 0, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B7": { - "x": 9, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C7": { - "x": 18, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D7": { - "x": 27, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E7": { - "x": 36, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F7": { - "x": 45, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G7": { - "x": 54, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H7": { - "x": 63, - "y": 54, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A8": { - "x": 0, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B8": { - "x": 9, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C8": { - "x": 18, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D8": { - "x": 27, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E8": { - "x": 36, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F8": { - "x": 45, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G8": { - "x": 54, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H8": { - "x": 63, - "y": 63, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A9": { - "x": 0, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B9": { - "x": 9, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C9": { - "x": 18, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D9": { - "x": 27, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E9": { - "x": 36, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F9": { - "x": 45, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G9": { - "x": 54, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H9": { - "x": 63, - "y": 72, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A10": { - "x": 0, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B10": { - "x": 9, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C10": { - "x": 18, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D10": { - "x": 27, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E10": { - "x": 36, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F10": { - "x": 45, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G10": { - "x": 54, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H10": { - "x": 63, - "y": 81, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A11": { - "x": 0, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B11": { - "x": 9, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C11": { - "x": 18, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D11": { - "x": 27, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E11": { - "x": 36, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F11": { - "x": 45, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G11": { - "x": 54, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H11": { - "x": 63, - "y": 90, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "A12": { - "x": 0, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "B12": { - "x": 9, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "C12": { - "x": 18, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "D12": { - "x": 27, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "E12": { - "x": 36, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "F12": { - "x": 45, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "G12": { - "x": 54, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - }, - "H12": { - "x": 63, - "y": 99, - "z": 0, - "depth": 2, - "diameter": 1, - "total-liquid-volume": 2 - } - } - }, - - "5ml-3x4": { - "origin-offset": { - "x": 18, - "y": 19 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "B1": { - "x": 25, - "y": 0, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "C1": { - "x": 50, - "y": 0, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "A2": { - "x": 0, - "y": 30, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "B2": { - "x": 25, - "y": 30, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "C2": { - "x": 50, - "y": 30, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "A3": { - "x": 0, - "y": 60, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "B3": { - "x": 25, - "y": 60, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "C3": { - "x": 50, - "y": 60, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "A4": { - "x": 0, - "y": 90, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "B4": { - "x": 25, - "y": 90, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - }, - "C4": { - "x": 50, - "y": 90, - "z": 0, - "depth": 55, - "diameter": 14, - "total-liquid-volume": 50000 - } - } - }, - - "small_vial_rack_16x45": { - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "B1": { - "x": 40, - "y": 0, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "C1": { - "x": 80, - "y": 0, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "D1": { - "x": 120, - "y": 0, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "A2": { - "x": 0, - "y": 20, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "B2": { - "x": 40, - "y": 20, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "C2": { - "x": 80, - "y": 20, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "D2": { - "x": 120, - "y": 20, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "A3": { - "x": 0, - "y": 40, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "B3": { - "x": 40, - "y": 40, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "C3": { - "x": 80, - "y": 40, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "D3": { - "x": 120, - "y": 40, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "A4": { - "x": 0, - "y": 60, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "B4": { - "x": 40, - "y": 60, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "C4": { - "x": 80, - "y": 60, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "D4": { - "x": 120, - "y": 60, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "A5": { - "x": 0, - "y": 80, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "B5": { - "x": 40, - "y": 80, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "C5": { - "x": 80, - "y": 80, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "D5": { - "x": 120, - "y": 80, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "A6": { - "x": 0, - "y": 100, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "B6": { - "x": 40, - "y": 100, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "C6": { - "x": 80, - "y": 100, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - }, - "D6": { - "x": 120, - "y": 100, - "z": 0, - "depth": 45, - "diameter": 16, - "total-liquid-volume": 10000 - } - } - }, - - "opentrons-tuberack-15ml": { - "origin-offset": { - "x": 34.375, - "y": 13.5 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "B1": { - "x": 25, - "y": 0, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "C1": { - "x": 50, - "y": 0, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "A2": { - "x": 0, - "y": 25, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "B2": { - "x": 25, - "y": 25, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "C2": { - "x": 50, - "y": 25, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "A3": { - "x": 0, - "y": 50, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "B3": { - "x": 25, - "y": 50, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "C3": { - "x": 50, - "y": 50, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "A4": { - "x": 0, - "y": 75, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "B4": { - "x": 25, - "y": 75, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "C4": { - "x": 50, - "y": 75, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "A5": { - "x": 0, - "y": 100, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "B5": { - "x": 25, - "y": 100, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - }, - "C5": { - "x": 50, - "y": 100, - "z": 6.78, - "depth": 117.98, - "diameter": 17, - "total-liquid-volume": 15000 - } - } - }, - - "opentrons-tuberack-50ml": { - "origin-offset": { - "x": 39.875, - "y": 37 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 6.95, - "depth": 112.85, - "diameter": 17, - "total-liquid-volume": 50000 - }, - "B1": { - "x": 35, - "y": 0, - "z": 6.95, - "depth": 112.85, - "diameter": 17, - "total-liquid-volume": 50000 - }, - "A2": { - "x": 0, - "y": 35, - "z": 6.95, - "depth": 112.85, - "diameter": 17, - "total-liquid-volume": 50000 - }, - "B2": { - "x": 35, - "y": 35, - "z": 6.95, - "depth": 112.85, - "diameter": 17, - "total-liquid-volume": 50000 - }, - "A3": { - "x": 0, - "y": 70, - "z": 6.95, - "depth": 112.85, - "diameter": 17, - "total-liquid-volume": 50000 - }, - "B3": { - "x": 35, - "y": 70, - "z": 6.95, - "depth": 112.85, - "diameter": 17, - "total-liquid-volume": 50000 - } - } - }, - - "opentrons-tuberack-15_50ml": { - "origin-offset": { - "x": 32.75, - "y": 14.875 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 6.78, - "depth": 117.98, - "diameter": 14.5, - "total-liquid-volume": 15000 - }, - "B1": { - "x": 25, - "y": 0, - "z": 6.78, - "depth": 117.98, - "diameter": 14.5, - "total-liquid-volume": 15000 - }, - "C1": { - "x": 50, - "y": 0, - "z": 6.78, - "depth": 117.98, - "diameter": 14.5, - "total-liquid-volume": 15000 - }, - "A2": { - "x": 0, - "y": 25, - "z": 6.78, - "depth": 117.98, - "diameter": 14.5, - "total-liquid-volume": 15000 - }, - "B2": { - "x": 25, - "y": 25, - "z": 6.78, - "depth": 117.98, - "diameter": 14.5, - "total-liquid-volume": 15000 - }, - "C2": { - "x": 50, - "y": 25, - "z": 6.78, - "depth": 117.98, - "diameter": 14.5, - "total-liquid-volume": 15000 - }, - "A3": { - "x": 18.25, - "y": 57.5, - "z": 6.95, - "depth": 112.85, - "diameter": 26.45, - "total-liquid-volume": 50000 - }, - "B3": { - "x": 53.25, - "y": 57.5, - "z": 6.95, - "depth": 112.85, - "diameter": 26.45, - "total-liquid-volume": 50000 - }, - "A4": { - "x": 18.25, - "y": 92.5, - "z": 6.95, - "depth": 112.85, - "diameter": 26.45, - "total-liquid-volume": 50000 - }, - "B4": { - "x": 53.25, - "y": 92.5, - "z": 6.95, - "depth": 112.85, - "diameter": 26.45, - "total-liquid-volume": 50000 - } - } - }, - - "opentrons-tuberack-2ml-eppendorf": { - "origin-offset": { - "x": 21.07, - "y": 18.21 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 19.28, - "y": 0, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 38.56, - "y": 0, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 57.84, - "y": 0, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A2": { - "x": 0, - "y": 19.89, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 19.28, - "y": 19.89, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 38.56, - "y": 19.89, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 57.84, - "y": 19.89, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A3": { - "x": 0, - "y": 39.78, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 19.28, - "y": 39.78, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 38.56, - "y": 39.78, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 57.84, - "y": 39.78, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A4": { - "x": 0, - "y": 59.67, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 19.28, - "y": 59.67, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 38.56, - "y": 59.67, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 57.84, - "y": 59.67, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A5": { - "x": 0, - "y": 79.56, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 19.28, - "y": 79.56, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 38.56, - "y": 79.56, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 57.84, - "y": 79.56, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A6": { - "x": 0, - "y": 99.45, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 19.28, - "y": 99.45, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 38.56, - "y": 99.45, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 57.84, - "y": 99.45, - "z": 43.3, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - } - } - }, - - "opentrons-tuberack-2ml-screwcap": { - "origin-offset": { - "x": 21.07, - "y": 18.21 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 19.28, - "y": 0, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 38.56, - "y": 0, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 57.84, - "y": 0, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "A2": { - "x": 0, - "y": 19.89, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 19.28, - "y": 19.89, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 38.56, - "y": 19.89, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 57.84, - "y": 19.89, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "A3": { - "x": 0, - "y": 39.78, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 19.28, - "y": 39.78, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 38.56, - "y": 39.78, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 57.84, - "y": 39.78, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "A4": { - "x": 0, - "y": 59.67, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 19.28, - "y": 59.67, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 38.56, - "y": 59.67, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 57.84, - "y": 59.67, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "A5": { - "x": 0, - "y": 79.56, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 19.28, - "y": 79.56, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 38.56, - "y": 79.56, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 57.84, - "y": 79.56, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "A6": { - "x": 0, - "y": 99.45, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 19.28, - "y": 99.45, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 38.56, - "y": 99.45, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 57.84, - "y": 99.45, - "z": 45.2, - "depth": 42, - "diameter": 8.5, - "total-liquid-volume": 2000 - } - } - }, - - "opentrons-tuberack-1.5ml-eppendorf": { - "origin-offset": { - "x": 21.07, - "y": 18.21 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "B1": { - "x": 19.28, - "y": 0, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "C1": { - "x": 38.56, - "y": 0, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "D1": { - "x": 57.84, - "y": 0, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "A2": { - "x": 0, - "y": 19.89, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "B2": { - "x": 19.28, - "y": 19.89, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "C2": { - "x": 38.56, - "y": 19.89, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "D2": { - "x": 57.84, - "y": 19.89, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "A3": { - "x": 0, - "y": 39.78, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "B3": { - "x": 19.28, - "y": 39.78, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "C3": { - "x": 38.56, - "y": 39.78, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "D3": { - "x": 57.84, - "y": 39.78, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "A4": { - "x": 0, - "y": 59.67, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "B4": { - "x": 19.28, - "y": 59.67, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "C4": { - "x": 38.56, - "y": 59.67, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "D4": { - "x": 57.84, - "y": 59.67, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "A5": { - "x": 0, - "y": 79.56, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "B5": { - "x": 19.28, - "y": 79.56, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "C5": { - "x": 38.56, - "y": 79.56, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "D5": { - "x": 57.84, - "y": 79.56, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "A6": { - "x": 0, - "y": 99.45, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "B6": { - "x": 19.28, - "y": 99.45, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "C6": { - "x": 38.56, - "y": 99.45, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - }, - "D6": { - "x": 57.84, - "y": 99.45, - "z": 43.3, - "depth": 37.0, - "diameter": 9, - "total-liquid-volume": 1500 - } - } - }, - - "opentrons-aluminum-block-2ml-eppendorf": { - "origin-offset": { - "x": 25.88, - "y": 20.75 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 17.25, - "y": 0, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 34.5, - "y": 0, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 51.75, - "y": 0, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A2": { - "x": 0, - "y": 17.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 17.25, - "y": 17.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 34.5, - "y": 17.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 51.75, - "y": 17.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A3": { - "x": 0, - "y": 34.5, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 17.25, - "y": 34.5, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 34.5, - "y": 34.5, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 51.75, - "y": 34.5, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A4": { - "x": 0, - "y": 51.75, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 17.25, - "y": 51.75, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 34.5, - "y": 51.75, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 51.75, - "y": 51.75, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A5": { - "x": 0, - "y": 69, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 17.25, - "y": 69, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 34.5, - "y": 69, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 51.75, - "y": 69, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A6": { - "x": 0, - "y": 86.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 17.25, - "y": 86.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 34.5, - "y": 86.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 51.75, - "y": 86.25, - "z": 5.5, - "depth": 38.5, - "diameter": 9, - "total-liquid-volume": 2000 - } - } - }, - "opentrons-aluminum-block-2ml-screwcap": { - "origin-offset": { - "x": 25.88, - "y": 20.75 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B1": { - "x": 17.25, - "y": 0, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C1": { - "x": 34.5, - "y": 0, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D1": { - "x": 51.75, - "y": 0, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A2": { - "x": 0, - "y": 17.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B2": { - "x": 17.25, - "y": 17.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C2": { - "x": 34.5, - "y": 17.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D2": { - "x": 51.75, - "y": 17.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A3": { - "x": 0, - "y": 34.5, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B3": { - "x": 17.25, - "y": 34.5, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C3": { - "x": 34.5, - "y": 34.5, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D3": { - "x": 51.75, - "y": 34.5, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A4": { - "x": 0, - "y": 51.75, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B4": { - "x": 17.25, - "y": 51.75, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C4": { - "x": 34.5, - "y": 51.75, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D4": { - "x": 51.75, - "y": 51.75, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A5": { - "x": 0, - "y": 69, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B5": { - "x": 17.25, - "y": 69, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C5": { - "x": 34.5, - "y": 69, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D5": { - "x": 51.75, - "y": 69, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "A6": { - "x": 0, - "y": 86.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "B6": { - "x": 17.25, - "y": 86.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "C6": { - "x": 34.5, - "y": 86.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - }, - "D6": { - "x": 51.75, - "y": 86.25, - "z": 6.5, - "depth": 42, - "diameter": 9, - "total-liquid-volume": 2000 - } - } - }, - "opentrons-aluminum-block-96-PCR-plate": { - "origin-offset": { - "x": 17.25, - "y": 13.38 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B1": { - "x": 9, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C1": { - "x": 18, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D1": { - "x": 27, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E1": { - "x": 36, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F1": { - "x": 45, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G1": { - "x": 54, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H1": { - "x": 63, - "y": 0, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A2": { - "x": 0, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B2": { - "x": 9, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C2": { - "x": 18, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D2": { - "x": 27, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E2": { - "x": 36, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F2": { - "x": 45, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G2": { - "x": 54, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H2": { - "x": 63, - "y": 9, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A3": { - "x": 0, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B3": { - "x": 9, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C3": { - "x": 18, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D3": { - "x": 27, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E3": { - "x": 36, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F3": { - "x": 45, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G3": { - "x": 54, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H3": { - "x": 63, - "y": 18, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A4": { - "x": 0, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B4": { - "x": 9, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C4": { - "x": 18, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D4": { - "x": 27, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E4": { - "x": 36, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F4": { - "x": 45, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G4": { - "x": 54, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H4": { - "x": 63, - "y": 27, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A5": { - "x": 0, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B5": { - "x": 9, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C5": { - "x": 18, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D5": { - "x": 27, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E5": { - "x": 36, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F5": { - "x": 45, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G5": { - "x": 54, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H5": { - "x": 63, - "y": 36, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A6": { - "x": 0, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B6": { - "x": 9, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C6": { - "x": 18, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D6": { - "x": 27, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E6": { - "x": 36, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F6": { - "x": 45, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G6": { - "x": 54, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H6": { - "x": 63, - "y": 45, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A7": { - "x": 0, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B7": { - "x": 9, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C7": { - "x": 18, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D7": { - "x": 27, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E7": { - "x": 36, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F7": { - "x": 45, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G7": { - "x": 54, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H7": { - "x": 63, - "y": 54, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A8": { - "x": 0, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B8": { - "x": 9, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C8": { - "x": 18, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D8": { - "x": 27, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E8": { - "x": 36, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F8": { - "x": 45, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G8": { - "x": 54, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H8": { - "x": 63, - "y": 63, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A9": { - "x": 0, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B9": { - "x": 9, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C9": { - "x": 18, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D9": { - "x": 27, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E9": { - "x": 36, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F9": { - "x": 45, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G9": { - "x": 54, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H9": { - "x": 63, - "y": 72, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A10": { - "x": 0, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B10": { - "x": 9, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C10": { - "x": 18, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D10": { - "x": 27, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E10": { - "x": 36, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F10": { - "x": 45, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G10": { - "x": 54, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H10": { - "x": 63, - "y": 81, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A11": { - "x": 0, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B11": { - "x": 9, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C11": { - "x": 18, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D11": { - "x": 27, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E11": { - "x": 36, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F11": { - "x": 45, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G11": { - "x": 54, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H11": { - "x": 63, - "y": 90, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "A12": { - "x": 0, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "B12": { - "x": 9, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "C12": { - "x": 18, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "D12": { - "x": 27, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "E12": { - "x": 36, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "F12": { - "x": 45, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "G12": { - "x": 54, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - }, - "H12": { - "x": 63, - "y": 99, - "z": 7.44, - "depth": 14.81, - "diameter": 5.4, - "total-liquid-volume": 200 - } - } - }, - "opentrons-aluminum-block-PCR-strips-200ul": { - "origin-offset": { - "x": 17.25, - "y": 13.38 - }, - "locations": { - "A1": { - "x": 0, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B1": { - "x": 9, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C1": { - "x": 18, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D1": { - "x": 27, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E1": { - "x": 36, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F1": { - "x": 45, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G1": { - "x": 54, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H1": { - "x": 63, - "y": 0, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A2": { - "x": 0, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B2": { - "x": 9, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C2": { - "x": 18, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D2": { - "x": 27, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E2": { - "x": 36, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F2": { - "x": 45, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G2": { - "x": 54, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H2": { - "x": 63, - "y": 9, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A3": { - "x": 0, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B3": { - "x": 9, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C3": { - "x": 18, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D3": { - "x": 27, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E3": { - "x": 36, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F3": { - "x": 45, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G3": { - "x": 54, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H3": { - "x": 63, - "y": 18, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A4": { - "x": 0, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B4": { - "x": 9, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C4": { - "x": 18, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D4": { - "x": 27, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E4": { - "x": 36, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F4": { - "x": 45, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G4": { - "x": 54, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H4": { - "x": 63, - "y": 27, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A5": { - "x": 0, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B5": { - "x": 9, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C5": { - "x": 18, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D5": { - "x": 27, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E5": { - "x": 36, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F5": { - "x": 45, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G5": { - "x": 54, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H5": { - "x": 63, - "y": 36, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A6": { - "x": 0, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B6": { - "x": 9, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C6": { - "x": 18, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D6": { - "x": 27, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E6": { - "x": 36, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F6": { - "x": 45, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G6": { - "x": 54, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H6": { - "x": 63, - "y": 45, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A7": { - "x": 0, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B7": { - "x": 9, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C7": { - "x": 18, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D7": { - "x": 27, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E7": { - "x": 36, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F7": { - "x": 45, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G7": { - "x": 54, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H7": { - "x": 63, - "y": 54, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A8": { - "x": 0, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B8": { - "x": 9, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C8": { - "x": 18, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D8": { - "x": 27, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E8": { - "x": 36, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F8": { - "x": 45, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G8": { - "x": 54, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H8": { - "x": 63, - "y": 63, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A9": { - "x": 0, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B9": { - "x": 9, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C9": { - "x": 18, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D9": { - "x": 27, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E9": { - "x": 36, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F9": { - "x": 45, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G9": { - "x": 54, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H9": { - "x": 63, - "y": 72, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A10": { - "x": 0, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B10": { - "x": 9, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C10": { - "x": 18, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D10": { - "x": 27, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E10": { - "x": 36, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F10": { - "x": 45, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G10": { - "x": 54, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H10": { - "x": 63, - "y": 81, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A11": { - "x": 0, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B11": { - "x": 9, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C11": { - "x": 18, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D11": { - "x": 27, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E11": { - "x": 36, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F11": { - "x": 45, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G11": { - "x": 54, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H11": { - "x": 63, - "y": 90, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "A12": { - "x": 0, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "B12": { - "x": 9, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "C12": { - "x": 18, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "D12": { - "x": 27, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "E12": { - "x": 36, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "F12": { - "x": 45, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "G12": { - "x": 54, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - }, - "H12": { - "x": 63, - "y": 99, - "z": 4.19, - "depth": 20.30, - "diameter": 5.4, - "total-liquid-volume": 300 - } - } - } - } -} diff --git a/api/src/opentrons/execute.py b/api/src/opentrons/execute.py index bd450db8086..7ce3a95d288 100644 --- a/api/src/opentrons/execute.py +++ b/api/src/opentrons/execute.py @@ -627,7 +627,9 @@ async def run(protocol_source: ProtocolSource) -> None: try: # TODO(mm, 2023-06-30): This will home and drop tips at the end, which is not how # things have historically behaved with PAPIv2.13 and older or JSONv5 and older. - result = await protocol_runner.run(protocol_source) + result = await protocol_runner.run( + deck_configuration=[], protocol_source=protocol_source + ) finally: unsubscribe() @@ -653,6 +655,8 @@ def _get_protocol_engine_config() -> Config: # We deliberately omit ignore_pause=True because, in the current implementation of # opentrons.protocol_api.core.engine, that would incorrectly make # ProtocolContext.is_simulating() return True. + use_simulated_deck_config=True, + # TODO the above is not correct for this and it should use the robot's actual config ) diff --git a/api/src/opentrons/hardware_control/api.py b/api/src/opentrons/hardware_control/api.py index 3aa91eaae5e..f3f70c16b9a 100644 --- a/api/src/opentrons/hardware_control/api.py +++ b/api/src/opentrons/hardware_control/api.py @@ -618,7 +618,7 @@ async def home(self, axes: Optional[List[Axis]] = None) -> None: if any(unsupported): raise UnsupportedHardwareCommand( message=f"At least one axis in {axes} is not supported on the OT2.", - detail={"unsupported_axes": unsupported}, + detail={"unsupported_axes": str(unsupported)}, ) self._reset_last_mount() # Initialize/update current_position @@ -661,14 +661,14 @@ async def current_position( raise PositionUnknownError( message=f"Current position of {str(mount)} pipette is unknown," " please home.", - detail={"mount": str(mount), "missing_axes": position_axes}, + detail={"mount": str(mount), "missing_axes": str(position_axes)}, ) axes_str = [ot2_axis_to_string(a) for a in position_axes] if not self._backend.is_homed(axes_str): unhomed = self._backend._unhomed_axes(axes_str) raise PositionUnknownError( message=f"{str(mount)} pipette axes ({unhomed}) must be homed.", - detail={"mount": str(mount), "unhomed_axes": unhomed}, + detail={"mount": str(mount), "unhomed_axes": str(unhomed)}, ) elif not self._current_position and not refresh: raise PositionUnknownError( @@ -755,7 +755,7 @@ async def move_axes( """ raise UnsupportedHardwareCommand( message="move_axes is not supported on the OT-2.", - detail={"axes_commanded": list(position.keys())}, + detail={"axes_commanded": str(list(position.keys()))}, ) async def move_rel( @@ -781,7 +781,7 @@ async def move_rel( " is unknown.", detail={ "mount": str(mount), - "fail_on_not_homed": fail_on_not_homed, + "fail_on_not_homed": str(fail_on_not_homed), }, ) else: @@ -797,7 +797,7 @@ async def move_rel( unhomed = self._backend._unhomed_axes(axes_str) raise PositionUnknownError( message=f"{str(mount)} pipette axes ({unhomed}) must be homed.", - detail={"mount": str(mount), "unhomed_axes": unhomed}, + detail={"mount": str(mount), "unhomed_axes": str(unhomed)}, ) await self._cache_and_maybe_retract_mount(mount) @@ -1084,6 +1084,38 @@ async def blow_out( blowout_spec.instr.set_current_volume(0) blowout_spec.instr.ready_to_aspirate = False + async def update_nozzle_configuration_for_mount( + self, + mount: top_types.Mount, + back_left_nozzle: Optional[str], + front_right_nozzle: Optional[str], + starting_nozzle: Optional[str] = None, + ) -> None: + """ + Update a nozzle configuration for a given pipette. + + The expectation of this function is that the back_left_nozzle/front_right_nozzle are the two corners + of a rectangle of nozzles. A call to this function that does not follow that schema will result + in an error. + + :param mount: A robot mount that the instrument is on. + :param back_left_nozzle: A string representing a nozzle name of the form such as 'A1'. + :param front_right_nozzle: A string representing a nozzle name of the form such as 'A1'. + :param starting_nozzle: A string representing the starting nozzle which will be used as the critical point + of the pipette nozzle configuration. By default, the back left nozzle will be the starting nozzle if + none is provided. + :return: None. + + If none of the nozzle parameters are provided, the nozzle configuration will be reset to default. + """ + if not back_left_nozzle and not front_right_nozzle and not starting_nozzle: + await self.reset_nozzle_configuration(mount) + else: + assert back_left_nozzle and front_right_nozzle + await self.update_nozzle_configuration( + mount, back_left_nozzle, front_right_nozzle, starting_nozzle + ) + async def pick_up_tip( self, mount: top_types.Mount, diff --git a/api/src/opentrons/hardware_control/backends/ot3controller.py b/api/src/opentrons/hardware_control/backends/ot3controller.py index 7c4ce56ac4f..9718e298dfd 100644 --- a/api/src/opentrons/hardware_control/backends/ot3controller.py +++ b/api/src/opentrons/hardware_control/backends/ot3controller.py @@ -1166,7 +1166,7 @@ def _pop_queue() -> Optional[Tuple[NodeId, ErrorCode]]: mount = Axis.to_ot3_mount(node_to_axis(q_msg[0])) raise PipetteOverpressureError( message=msg.format(str(mount)), - detail={"mount": mount}, + detail={"mount": str(mount)}, ) else: yield diff --git a/api/src/opentrons/hardware_control/dev_types.py b/api/src/opentrons/hardware_control/dev_types.py index 915761245e8..0c4e5ae4ef7 100644 --- a/api/src/opentrons/hardware_control/dev_types.py +++ b/api/src/opentrons/hardware_control/dev_types.py @@ -28,6 +28,7 @@ from opentrons.drivers.types import MoveSplit from opentrons.types import Mount from opentrons.hardware_control.types import GripperJawState +from opentrons.hardware_control.nozzle_manager import NozzleMap class InstrumentSpec(TypedDict): @@ -94,6 +95,7 @@ class PipetteDict(InstrumentDict): has_tip: bool default_push_out_volume: Optional[float] supported_tips: Dict[PipetteTipType, SupportedTipsDefinition] + current_nozzle_map: Optional[NozzleMap] class PipetteStateDict(TypedDict): diff --git a/api/src/opentrons/hardware_control/errors.py b/api/src/opentrons/hardware_control/errors.py index f678167cf28..6cde3fa076b 100644 --- a/api/src/opentrons/hardware_control/errors.py +++ b/api/src/opentrons/hardware_control/errors.py @@ -25,7 +25,7 @@ class InvalidPipetteName(InvalidInstrumentData): def __init__(self, name: int, mount: str) -> None: super().__init__( message=f"Invalid pipette name key {name} on mount {mount}", - detail={"mount": mount, "name": name}, + detail={"mount": mount, "name": str(name)}, ) diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py index 5185fce9f3e..b529956e569 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette.py @@ -5,14 +5,15 @@ """ Classes and functions for pipette state tracking """ import logging -from typing import Any, Dict, Optional, Set, Tuple, Union, cast, List +from typing import Any, Dict, Optional, Set, Tuple, Union, cast from opentrons_shared_data.pipette.pipette_definition import ( PipetteConfigurations, PlungerPositions, MotorConfigurations, SupportedTipsDefinition, - TipHandlingConfigurations, + PickUpTipConfigurations, + DropTipConfigurations, PipetteModelVersionType, PipetteNameType, PipetteLiquidPropertiesDefinition, @@ -46,6 +47,7 @@ BoardRevision, ) from opentrons.hardware_control.errors import InvalidCriticalPoint +from opentrons.hardware_control import nozzle_manager from opentrons_shared_data.pipette.dev_types import ( @@ -54,7 +56,6 @@ PipetteModel, ) from opentrons.hardware_control.dev_types import InstrumentHardwareConfigs -from typing_extensions import Final RECONFIG_KEYS = {"quirks"} @@ -62,8 +63,6 @@ mod_log = logging.getLogger(__name__) -INTERNOZZLE_SPACING_MM: Final[float] = 9 - # TODO (lc 11-1-2022) Once we unify calibration loading # for the hardware controller, we will be able to # unify the pipette classes again. @@ -92,6 +91,7 @@ def __init__( self._pipette_version = self._config.version self._max_channels = self._config.channels self._backlash_distance = config.backlash_distance + self._pick_up_configurations = config.pick_up_tip_configurations self._liquid_class_name = pip_types.LiquidClasses.default self._liquid_class = self._config.liquid_properties[self._liquid_class_name] @@ -109,6 +109,9 @@ def __init__( pipette_version=config.version, ) self._nozzle_offset = self._config.nozzle_offset + self._nozzle_manager = ( + nozzle_manager.NozzleConfigurationManager.build_from_config(self._config) + ) self._current_volume = 0.0 self._working_volume = float(self._liquid_class.max_volume) self._current_tip_length = 0.0 @@ -197,8 +200,12 @@ def liquid_class_name(self) -> pip_types.LiquidClasses: return self._liquid_class_name @property - def nozzle_offset(self) -> List[float]: - return self._nozzle_offset + def nozzle_offset(self) -> Point: + return self._nozzle_manager.starting_nozzle_offset + + @property + def nozzle_manager(self) -> nozzle_manager.NozzleConfigurationManager: + return self._nozzle_manager @property def pipette_offset(self) -> PipetteOffsetByPipetteMount: @@ -221,17 +228,15 @@ def plunger_motor_current(self) -> MotorConfigurations: return self._config.plunger_motor_configurations @property - def pick_up_configurations(self) -> TipHandlingConfigurations: + def pick_up_configurations(self) -> PickUpTipConfigurations: return self._config.pick_up_tip_configurations @pick_up_configurations.setter - def pick_up_configurations( - self, pick_up_configs: TipHandlingConfigurations - ) -> None: + def pick_up_configurations(self, pick_up_configs: PickUpTipConfigurations) -> None: self._pick_up_configurations = pick_up_configs @property - def drop_configurations(self) -> TipHandlingConfigurations: + def drop_configurations(self) -> DropTipConfigurations: return self._config.drop_tip_configurations @property @@ -282,6 +287,9 @@ def reset_state(self) -> None: ) self._tip_overlap_lookup = self.liquid_class.tip_overlap_dictionary + self._nozzle_manager = ( + nozzle_manager.NozzleConfigurationManager.build_from_config(self._config) + ) def reset_pipette_offset(self, mount: Mount, to_default: bool) -> None: """Reset the pipette offset to system defaults.""" @@ -323,8 +331,6 @@ def critical_point(self, cp_override: Optional[CriticalPoint] = None) -> Point: we have a tip, or :py:attr:`CriticalPoint.XY_CENTER` - the specified critical point will be used. """ - instr = Point(*self._pipette_offset.offset) - offsets = self.nozzle_offset if cp_override in [ CriticalPoint.GRIPPER_JAW_CENTER, @@ -333,42 +339,22 @@ def critical_point(self, cp_override: Optional[CriticalPoint] = None) -> Point: ]: raise InvalidCriticalPoint(cp_override.name, "pipette") - if not self.has_tip or cp_override == CriticalPoint.NOZZLE: - cp_type = CriticalPoint.NOZZLE - tip_length = 0.0 - else: - cp_type = CriticalPoint.TIP - tip_length = self.current_tip_length - if cp_override == CriticalPoint.XY_CENTER: - mod_offset_xy = [ - offsets[0], - offsets[1] - (INTERNOZZLE_SPACING_MM * (self._config.channels - 1) / 2), - offsets[2], - ] - cp_type = CriticalPoint.XY_CENTER - elif cp_override == CriticalPoint.FRONT_NOZZLE: - mod_offset_xy = [ - 0, - (offsets[1] - INTERNOZZLE_SPACING_MM * (self._config.channels - 1)), - offsets[2], - ] - cp_type = CriticalPoint.FRONT_NOZZLE - else: - mod_offset_xy = list(offsets) - mod_and_tip = Point( - mod_offset_xy[0], mod_offset_xy[1], mod_offset_xy[2] - tip_length + instr = Point(*self._pipette_offset.offset) + cp_with_tip_length = self._nozzle_manager.critical_point_with_tip_length( + cp_override, + self.current_tip_length if cp_override != CriticalPoint.NOZZLE else 0.0, ) - cp = mod_and_tip + instr + cp = cp_with_tip_length + instr if self._log.isEnabledFor(logging.DEBUG): info_str = "cp: {}{}: {} (from: ".format( - cp_type, " (from override)" if cp_override else "", cp + cp_override, " (from override)" if cp_override else "", cp ) info_str += "model offset: {} + instrument offset: {}".format( - mod_offset_xy, instr + cp_with_tip_length, instr ) - info_str += " - tip_length: {}".format(tip_length) + info_str += " - tip_length: {}".format(self.current_tip_length) info_str += ")" self._log.debug(info_str) @@ -485,6 +471,25 @@ def ok_to_push_out(self, push_out_dist_mm: float) -> bool: self.plunger_positions.bottom - self.plunger_positions.blow_out ) + def update_nozzle_configuration( + self, + back_left_nozzle: str, + front_right_nozzle: str, + starting_nozzle: Optional[str] = None, + ) -> None: + """ + Update nozzle configuration manager. + """ + self._nozzle_manager.update_nozzle_configuration( + back_left_nozzle, front_right_nozzle, starting_nozzle + ) + + def reset_nozzle_configuration(self) -> None: + """ + Reset nozzle configuration manager. + """ + self._nozzle_manager.reset_to_default_configuration() + def add_tip(self, tip_length: float) -> None: """ Add a tip to the pipette for position tracking and validation diff --git a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py index dba4e253da6..d2a36f19e85 100644 --- a/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot2/pipette_handler.py @@ -226,6 +226,7 @@ def get_attached_instrument(self, mount: MountType) -> PipetteDict: # this dict newly every time? Any why only a few items are being updated? for key in configs: result[key] = instr_dict[key] + result["current_nozzle_map"] = instr.nozzle_manager.current_configuration result["min_volume"] = instr.liquid_class.min_volume result["max_volume"] = instr.liquid_class.max_volume result["channels"] = instr.channels @@ -392,6 +393,24 @@ async def reset(self) -> None: k: None for k in self._attached_instruments.keys() } + async def update_nozzle_configuration( + self, + mount: MountType, + back_left_nozzle: str, + front_right_nozzle: str, + starting_nozzle: Optional[str] = None, + ) -> None: + instr = self._attached_instruments[mount] + if instr: + instr.update_nozzle_configuration( + back_left_nozzle, front_right_nozzle, starting_nozzle + ) + + async def reset_nozzle_configuration(self, mount: MountType) -> None: + instr = self._attached_instruments[mount] + if instr: + instr.reset_nozzle_configuration() + async def add_tip(self, mount: MountType, tip_length: float) -> None: instr = self._attached_instruments[mount] attached = self.attached_instruments @@ -619,7 +638,7 @@ def plan_check_dispense( # type: ignore[no-untyped-def] message="Cannot push_out on a dispense that does not leave the pipette empty", detail={ "command": "dispense", - "remaining-volume": instrument.current_volume - disp_vol, + "remaining-volume": str(instrument.current_volume - disp_vol), }, ) push_out_ul = 0 @@ -749,16 +768,16 @@ def plan_check_pick_up_tip( # type: ignore[no-untyped-def] self._ihp_log.debug(f"Picking up tip on {mount.name}") if presses is None or presses < 0: - checked_presses = instrument.pick_up_configurations.presses + checked_presses = instrument.pick_up_configurations.press_fit.presses else: checked_presses = presses if not increment or increment < 0: - check_incr = instrument.pick_up_configurations.increment + check_incr = instrument.pick_up_configurations.press_fit.increment else: check_incr = increment - pick_up_speed = instrument.pick_up_configurations.speed + pick_up_speed = instrument.pick_up_configurations.press_fit.speed def build_presses() -> Iterator[Tuple[float, float]]: # Press the nozzle into the tip number of times, @@ -766,7 +785,7 @@ def build_presses() -> Iterator[Tuple[float, float]]: for i in range(checked_presses): # move nozzle down into the tip press_dist = ( - -1.0 * instrument.pick_up_configurations.distance + -1.0 * instrument.pick_up_configurations.press_fit.distance + -1.0 * check_incr * i ) # move nozzle back up @@ -789,7 +808,9 @@ def add_tip_to_instr() -> None: current={ Axis.by_mount( mount - ): instrument.pick_up_configurations.current + ): instrument.pick_up_configurations.press_fit.current_by_tip_count[ + instrument.nozzle_manager.current_configuration.tip_count + ] }, speed=pick_up_speed, relative_down=top_types.Point(0, 0, press_dist), @@ -798,7 +819,7 @@ def add_tip_to_instr() -> None: for press_dist, backup_dist in build_presses() ], shake_off_list=self._build_pickup_shakes(instrument), - retract_target=instrument.pick_up_configurations.distance + retract_target=instrument.pick_up_configurations.press_fit.distance + check_incr * checked_presses + 2, ), @@ -818,7 +839,9 @@ def add_tip_to_instr() -> None: current={ Axis.by_mount( mount - ): instrument.pick_up_configurations.current + ): instrument.pick_up_configurations.press_fit.current_by_tip_count[ + instrument.nozzle_manager.current_configuration.tip_count + ] }, speed=pick_up_speed, relative_down=top_types.Point(0, 0, press_dist), @@ -827,7 +850,7 @@ def add_tip_to_instr() -> None: for press_dist, backup_dist in build_presses() ], shake_off_list=self._build_pickup_shakes(instrument), - retract_target=instrument.pick_up_configurations.distance + retract_target=instrument.pick_up_configurations.press_fit.distance + check_incr * checked_presses + 2, ), @@ -904,9 +927,13 @@ def plan_check_drop_tip( # type: ignore[no-untyped-def] ): instrument = self.get_pipette(mount) + if not instrument.drop_configurations.plunger_eject: + raise CommandPreconditionViolated( + f"Pipette {instrument.name} on {mount.name} has no plunger eject configuration" + ) bottom = instrument.plunger_positions.bottom droptip = instrument.plunger_positions.drop_tip - speed = instrument.drop_configurations.speed + speed = instrument.drop_configurations.plunger_eject.speed shakes: List[Tuple[top_types.Point, Optional[float]]] = [] if Quirks.dropTipShake in instrument.config.quirks: diameter = instrument.current_tiprack_diameter @@ -922,7 +949,11 @@ def _remove_tips() -> None: bottom, droptip, {Axis.of_plunger(mount): instrument.plunger_motor_current.run}, - {Axis.of_plunger(mount): instrument.drop_configurations.current}, + { + Axis.of_plunger( + mount + ): instrument.drop_configurations.plunger_eject.current + }, speed, home_after, (Axis.of_plunger(mount),), @@ -952,7 +983,7 @@ def _remove_tips() -> None: { Axis.of_main_tool_actuator( mount - ): instrument.drop_configurations.current + ): instrument.drop_configurations.plunger_eject.current }, speed, home_after, diff --git a/api/src/opentrons/hardware_control/instruments/ot3/gripper.py b/api/src/opentrons/hardware_control/instruments/ot3/gripper.py index 7eb757fd333..3eb3c863522 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/gripper.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/gripper.py @@ -183,16 +183,16 @@ def check_calibration_pin_location_is_accurate(self) -> None: raise CommandPreconditionViolated( "Cannot calibrate gripper without attaching a calibration probe", detail={ - "probe": self._attached_probe, - "jaw_state": self.state, + "probe": str(self._attached_probe), + "jaw_state": str(self.state), }, ) if self.state != GripperJawState.GRIPPING: raise CommandPreconditionViolated( "Cannot calibrate gripper if jaw is not in gripping state", detail={ - "probe": self._attached_probe, - "jaw_state": self.state, + "probe": str(self._attached_probe), + "jaw_state": str(self.state), }, ) diff --git a/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py b/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py index 51778b08b92..cf2ba55e23d 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/gripper_handler.py @@ -133,7 +133,7 @@ def check_ready_for_jaw_move(self, command: str) -> None: message=f"Cannot {command} gripper jaw before homing", detail={ "command": command, - "jaw_state": gripper.state, + "jaw_state": str(gripper.state), }, ) diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py index 272a1dd97ec..a89ba96290e 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py @@ -1,8 +1,7 @@ import logging import functools -from typing import Any, List, Dict, Optional, Set, Tuple, Union, cast -from typing_extensions import Final +from typing import Any, Dict, Optional, Set, Tuple, Union, cast from opentrons.types import Point @@ -12,7 +11,10 @@ PlungerPositions, MotorConfigurations, SupportedTipsDefinition, - TipHandlingConfigurations, + PickUpTipConfigurations, + PressFitPickUpTipConfiguration, + CamActionPickUpTipConfiguration, + DropTipConfigurations, PlungerHomingConfigurations, PipetteNameType, PipetteModelVersionType, @@ -46,12 +48,10 @@ ) from opentrons.hardware_control.types import CriticalPoint, OT3Mount from opentrons.hardware_control.errors import InvalidCriticalPoint +from opentrons.hardware_control import nozzle_manager mod_log = logging.getLogger(__name__) -# TODO (lc 12-2-2022) We should move this to the geometry configurations -INTERNOZZLE_SPACING_MM: Final[float] = 9 - class Pipette(AbstractInstrument[PipetteConfigurations]): """A class to gather and track pipette state and configs. @@ -97,6 +97,9 @@ def __init__( pipette_version=config.version, ) self._nozzle_offset = self._config.nozzle_offset + self._nozzle_manager = ( + nozzle_manager.NozzleConfigurationManager.build_from_config(self._config) + ) self._current_volume = 0.0 self._working_volume = float(self._liquid_class.max_volume) self._current_tip_length = 0.0 @@ -162,8 +165,12 @@ def tip_overlap(self) -> Dict[str, float]: return self._tip_overlap_lookup @property - def nozzle_offset(self) -> List[float]: - return self._nozzle_offset + def nozzle_offset(self) -> Point: + return self._nozzle_manager.starting_nozzle_offset + + @property + def nozzle_manager(self) -> nozzle_manager.NozzleConfigurationManager: + return self._nozzle_manager @property def pipette_offset(self) -> PipetteOffsetByPipetteMount: @@ -178,13 +185,11 @@ def plunger_motor_current(self) -> MotorConfigurations: return self._plunger_motor_current @property - def pick_up_configurations(self) -> TipHandlingConfigurations: + def pick_up_configurations(self) -> PickUpTipConfigurations: return self._pick_up_configurations @pick_up_configurations.setter - def pick_up_configurations( - self, pick_up_configs: TipHandlingConfigurations - ) -> None: + def pick_up_configurations(self, pick_up_configs: PickUpTipConfigurations) -> None: self._pick_up_configurations = pick_up_configs @property @@ -192,7 +197,7 @@ def plunger_homing_configurations(self) -> PlungerHomingConfigurations: return self._plunger_homing_configurations @property - def drop_configurations(self) -> TipHandlingConfigurations: + def drop_configurations(self) -> DropTipConfigurations: return self._drop_configurations @property @@ -250,6 +255,9 @@ def reset_state(self) -> None: self._flow_acceleration = self._active_tip_settings.default_flow_acceleration self._tip_overlap_lookup = self.liquid_class.tip_overlap_dictionary + self._nozzle_manager = ( + nozzle_manager.NozzleConfigurationManager.build_from_config(self._config) + ) def reset_pipette_offset(self, mount: OT3Mount, to_default: bool) -> None: """Reset the pipette offset to system defaults.""" @@ -293,73 +301,28 @@ def critical_point(self, cp_override: Optional[CriticalPoint] = None) -> Point: we have a tip, or :py:attr:`CriticalPoint.XY_CENTER` - the specified critical point will be used. """ - instr = Point(*self._pipette_offset.offset) - offsets = self.nozzle_offset - # Temporary solution for the 96 channel critical point locations. - # We should instead record every channel "critical point" in - # the pipette configurations. - X_DIRECTION_VALUE = 1 - Y_DIVISION = 2 - if self.channels == 96: - NUM_ROWS = 12 - NUM_COLS = 8 - X_DIRECTION_VALUE = -1 - elif self.channels == 8: - NUM_ROWS = 1 - NUM_COLS = 8 - else: - NUM_ROWS = 1 - NUM_COLS = 1 - - x_offset_to_right_nozzle = ( - X_DIRECTION_VALUE * INTERNOZZLE_SPACING_MM * (NUM_ROWS - 1) - ) - y_offset_to_front_nozzle = INTERNOZZLE_SPACING_MM * (NUM_COLS - 1) - if cp_override in [ CriticalPoint.GRIPPER_JAW_CENTER, CriticalPoint.GRIPPER_FRONT_CALIBRATION_PIN, CriticalPoint.GRIPPER_REAR_CALIBRATION_PIN, ]: raise InvalidCriticalPoint(cp_override.name, "pipette") - if not self.has_tip_length or cp_override == CriticalPoint.NOZZLE: - cp_type = CriticalPoint.NOZZLE - tip_length = 0.0 - else: - cp_type = CriticalPoint.TIP - tip_length = self.current_tip_length - if cp_override == CriticalPoint.XY_CENTER: - mod_offset_xy = [ - offsets[0] - x_offset_to_right_nozzle / 2, - offsets[1] - y_offset_to_front_nozzle / Y_DIVISION, - offsets[2], - ] - cp_type = CriticalPoint.XY_CENTER - elif cp_override == CriticalPoint.FRONT_NOZZLE: - # front left nozzle of the 96 channel and - # front nozzle of the 8 channel - mod_offset_xy = [ - offsets[0], - offsets[1] - y_offset_to_front_nozzle, - offsets[2], - ] - cp_type = CriticalPoint.FRONT_NOZZLE - else: - mod_offset_xy = list(offsets) - mod_and_tip = Point( - mod_offset_xy[0], mod_offset_xy[1], mod_offset_xy[2] - tip_length - ) - cp = mod_and_tip + instr + instr = Point(*self._pipette_offset.offset) + cp_with_tip_length = self._nozzle_manager.critical_point_with_tip_length( + cp_override, + self.current_tip_length if cp_override != CriticalPoint.NOZZLE else 0.0, + ) + cp = cp_with_tip_length + instr if self._log.isEnabledFor(logging.DEBUG): info_str = "cp: {}{}: {} (from: ".format( - cp_type, " (from override)" if cp_override else "", cp + cp_override, " (from override)" if cp_override else "", cp ) info_str += "model offset: {} + instrument offset: {}".format( - mod_offset_xy, instr + cp_with_tip_length, instr ) - info_str += " - tip_length: {}".format(tip_length) + info_str += " - tip_length: {}".format(self.current_tip_length) info_str += ")" self._log.debug(info_str) @@ -483,6 +446,25 @@ def ok_to_push_out(self, push_out_dist_mm: float) -> bool: self.plunger_positions.blow_out - self.plunger_positions.bottom ) + def update_nozzle_configuration( + self, + back_left_nozzle: str, + front_right_nozzle: str, + starting_nozzle: Optional[str] = None, + ) -> None: + """ + Update nozzle configuration manager. + """ + self._nozzle_manager.update_nozzle_configuration( + back_left_nozzle, front_right_nozzle, starting_nozzle + ) + + def reset_nozzle_configuration(self) -> None: + """ + Reset nozzle configuration manager. + """ + self._nozzle_manager.reset_to_default_configuration() + def add_tip(self, tip_length: float) -> None: """ Add a tip to the pipette for position tracking and validation @@ -523,14 +505,6 @@ def tip_presence_responses(self) -> int: # TODO: put this in shared-data return 2 if self.channels > 8 else 1 - @property - def connect_tiprack_distance_mm(self) -> float: - return self._config.connect_tiprack_distance_mm - - @property - def end_tip_action_retract_distance_mm(self) -> float: - return self._config.end_tip_action_retract_distance_mm - # Cache max is chosen somewhat arbitrarily. With a float is input we don't # want this to unbounded. @functools.lru_cache(maxsize=100) @@ -610,7 +584,7 @@ def set_liquid_class_by_name(self, class_name: str) -> None: message=f"Liquid class {class_name} is not valid for {self._config.display_name}", detail={ "requested-class-name": class_name, - "pipette-model": self._pipette_model, + "pipette-model": str(self._pipette_model), }, ) if ( @@ -685,6 +659,21 @@ def set_tip_type(self, tip_type: pip_types.PipetteTipType) -> None: self._tip_overlap_lookup = self.liquid_class.tip_overlap_dictionary self._working_volume = min(tip_type.value, self.liquid_class.max_volume) + def get_pick_up_configuration_for_tip_count( + self, count: int + ) -> Union[CamActionPickUpTipConfiguration, PressFitPickUpTipConfiguration]: + for config in ( + self._config.pick_up_tip_configurations.press_fit, + self._config.pick_up_tip_configurations.cam_action, + ): + if not config: + continue + if count in config.current_by_tip_count: + return config + raise CommandPreconditionViolated( + message=f"No pick up tip configuration for {count} tips", + ) + def _reload_and_check_skip( new_config: PipetteConfigurations, diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py index ff8fbc64122..006417484d4 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette_handler.py @@ -22,6 +22,8 @@ ) from opentrons_shared_data.pipette.pipette_definition import ( liquid_class_for_volume_between_default_and_defaultlowvolume, + PressFitPickUpTipConfiguration, + CamActionPickUpTipConfiguration, ) from opentrons import types as top_types @@ -241,6 +243,7 @@ def get_attached_instrument(self, mount: OT3Mount) -> PipetteDict: for key in configs: result[key] = instr_dict[key] + result["current_nozzle_map"] = instr.nozzle_manager.current_configuration result["min_volume"] = instr.liquid_class.min_volume result["max_volume"] = instr.liquid_class.max_volume result["channels"] = instr._max_channels @@ -398,6 +401,24 @@ async def reset(self) -> None: k: None for k in self._attached_instruments.keys() } + async def update_nozzle_configuration( + self, + mount: MountType, + back_left_nozzle: str, + front_right_nozzle: str, + starting_nozzle: Optional[str] = None, + ) -> None: + instr = self._attached_instruments[OT3Mount.from_mount(mount)] + if instr: + instr.update_nozzle_configuration( + back_left_nozzle, front_right_nozzle, starting_nozzle + ) + + async def reset_nozzle_configuration(self, mount: OT3Mount) -> None: + instr = self._attached_instruments[OT3Mount.from_mount(mount)] + if instr: + instr.reset_nozzle_configuration() + async def add_tip(self, mount: OT3Mount, tip_length: float) -> None: instr = self._attached_instruments[mount] attached = self.attached_instruments @@ -594,7 +615,7 @@ def plan_check_dispense( message="Cannot push_out on a dispense that does not leave the pipette empty", detail={ "command": "dispense", - "remaining-volume": instrument.current_volume - disp_vol, + "remaining-volume": str(instrument.current_volume - disp_vol), }, ) push_out_ul = 0 @@ -710,7 +731,7 @@ def build_one_shake() -> List[Tuple[top_types.Point, Optional[float]]]: return [] - def plan_ht_pick_up_tip(self) -> TipActionSpec: + def plan_ht_pick_up_tip(self, tip_count: int) -> TipActionSpec: # Prechecks: ready for pickup tip and press/increment are valid mount = OT3Mount.LEFT instrument = self.get_pipette(mount) @@ -718,25 +739,32 @@ def plan_ht_pick_up_tip(self) -> TipActionSpec: raise UnexpectedTipAttachError("pick_up_tip", instrument.name, mount.name) self._ihp_log.debug(f"Picking up tip on {mount.name}") + pick_up_config = instrument.get_pick_up_configuration_for_tip_count(tip_count) + if not isinstance(pick_up_config, CamActionPickUpTipConfiguration): + raise CommandPreconditionViolated( + f"Low-throughput pick up tip got wrong config for {instrument.name} on {mount.name}" + ) + tip_motor_moves = self._build_tip_motor_moves( - prep_move_dist=instrument.pick_up_configurations.prep_move_distance, - clamp_move_dist=instrument.pick_up_configurations.distance, - prep_move_speed=instrument.pick_up_configurations.prep_move_speed, - clamp_move_speed=instrument.pick_up_configurations.speed, + prep_move_dist=pick_up_config.prep_move_distance, + clamp_move_dist=pick_up_config.distance, + prep_move_speed=pick_up_config.prep_move_speed, + clamp_move_speed=pick_up_config.speed, plunger_current=instrument.plunger_motor_current.run, - tip_motor_current=instrument.pick_up_configurations.current, + tip_motor_current=pick_up_config.current_by_tip_count[tip_count], ) return TipActionSpec( tip_action_moves=tip_motor_moves, shake_off_moves=[], - z_distance_to_tiprack=(-1 * instrument.connect_tiprack_distance_mm), - ending_z_retract_distance=instrument.end_tip_action_retract_distance_mm, + z_distance_to_tiprack=(-1 * pick_up_config.connect_tiprack_distance_mm), + ending_z_retract_distance=instrument.config.end_tip_action_retract_distance_mm, ) def plan_lt_pick_up_tip( self, mount: OT3Mount, + tip_count: int, presses: Optional[int], increment: Optional[float], ) -> TipActionSpec: @@ -746,17 +774,22 @@ def plan_lt_pick_up_tip( raise UnexpectedTipAttachError("pick_up_tip", instrument.name, mount.name) self._ihp_log.debug(f"Picking up tip on {mount.name}") + pick_up_config = instrument.get_pick_up_configuration_for_tip_count(tip_count) + if not isinstance(pick_up_config, PressFitPickUpTipConfiguration): + raise CommandPreconditionViolated( + f"Low-throughput pick up tip got wrong config for {instrument.name} on {mount.name}" + ) if presses is None or presses < 0: - checked_presses = instrument.pick_up_configurations.presses + checked_presses = pick_up_config.presses else: checked_presses = presses if not increment or increment < 0: - check_incr = instrument.pick_up_configurations.increment + check_incr = pick_up_config.increment else: check_incr = increment - pick_up_speed = instrument.pick_up_configurations.speed + pick_up_speed = pick_up_config.speed def build_presses() -> List[TipActionMoveSpec]: # Press the nozzle into the tip number of times, @@ -764,18 +797,15 @@ def build_presses() -> List[TipActionMoveSpec]: press_moves = [] for i in range(checked_presses): # move nozzle down into the tip - press_dist = ( - -1.0 * instrument.pick_up_configurations.distance - + -1.0 * check_incr * i - ) + press_dist = -1.0 * pick_up_config.distance + -1.0 * check_incr * i press_moves.append( TipActionMoveSpec( distance=press_dist, speed=pick_up_speed, currents={ - Axis.by_mount( - mount - ): instrument.pick_up_configurations.current + Axis.by_mount(mount): pick_up_config.current_by_tip_count[ + tip_count + ] }, ) ) @@ -821,15 +851,17 @@ def plan_lt_drop_tip( mount: OT3Mount, ) -> TipActionSpec: instrument = self.get_pipette(mount) - + config = instrument.drop_configurations.plunger_eject + if not config: + raise CommandPreconditionViolated( + f"No plunger-eject drop tip configurations for {instrument.name} on {mount.name}" + ) drop_seq = [ TipActionMoveSpec( distance=instrument.plunger_positions.drop_tip, - speed=instrument.drop_configurations.speed, + speed=config.speed, currents={ - Axis.of_main_tool_actuator( - mount - ): instrument.drop_configurations.current, + Axis.of_main_tool_actuator(mount): config.current, }, ), TipActionMoveSpec( @@ -851,14 +883,19 @@ def plan_lt_drop_tip( def plan_ht_drop_tip(self) -> TipActionSpec: mount = OT3Mount.LEFT instrument = self.get_pipette(mount) + config = instrument.drop_configurations.cam_action + if not config: + raise CommandPreconditionViolated( + f"No cam-action drop tip configurations for {instrument.name} on {mount.name}" + ) drop_seq = self._build_tip_motor_moves( - prep_move_dist=instrument.drop_configurations.prep_move_distance, - clamp_move_dist=instrument.drop_configurations.distance, - prep_move_speed=instrument.drop_configurations.prep_move_speed, - clamp_move_speed=instrument.drop_configurations.speed, + prep_move_dist=config.prep_move_distance, + clamp_move_dist=config.distance, + prep_move_speed=config.prep_move_speed, + clamp_move_speed=config.speed, plunger_current=instrument.plunger_motor_current.run, - tip_motor_current=instrument.drop_configurations.current, + tip_motor_current=config.current, ) return TipActionSpec( diff --git a/api/src/opentrons/hardware_control/nozzle_manager.py b/api/src/opentrons/hardware_control/nozzle_manager.py new file mode 100644 index 00000000000..4841a1fdee8 --- /dev/null +++ b/api/src/opentrons/hardware_control/nozzle_manager.py @@ -0,0 +1,326 @@ +from typing import Dict, List, Optional, Any, Sequence, Iterator, Tuple, cast +from dataclasses import dataclass +from collections import OrderedDict +from enum import Enum +from itertools import chain + +from opentrons.hardware_control.types import CriticalPoint +from opentrons.types import Point +from opentrons_shared_data.pipette.pipette_definition import ( + PipetteGeometryDefinition, + PipetteRowDefinition, +) +from opentrons_shared_data.errors import ErrorCodes, GeneralError, PythonException + + +def _nozzle_names_by_row(rows: List[PipetteRowDefinition]) -> Iterator[str]: + for row in rows: + for nozzle in row.ordered_nozzles: + yield nozzle + + +def _row_or_col_index_for_nozzle( + row_or_col: "OrderedDict[str, List[str]]", nozzle: str +) -> int: + for index, row_or_col_contents in enumerate(row_or_col.values()): + if nozzle in row_or_col_contents: + return index + raise KeyError(nozzle) + + +def _row_col_indices_for_nozzle( + rows: "OrderedDict[str, List[str]]", + cols: "OrderedDict[str, List[str]]", + nozzle: str, +) -> Tuple[int, int]: + return _row_or_col_index_for_nozzle(rows, nozzle), _row_or_col_index_for_nozzle( + cols, nozzle + ) + + +class NozzleConfigurationType(Enum): + """ + Nozzle Configuration Type. + + Represents the current nozzle + configuration stored in NozzleMap + """ + + COLUMN = "COLUMN" + ROW = "ROW" + SINGLE = "SINGLE" + FULL = "FULL" + SUBRECT = "SUBRECT" + + @classmethod + def determine_nozzle_configuration( + cls, + physical_rows: "OrderedDict[str, List[str]]", + current_rows: "OrderedDict[str, List[str]]", + physical_cols: "OrderedDict[str, List[str]]", + current_cols: "OrderedDict[str, List[str]]", + ) -> "NozzleConfigurationType": + """ + Determine the nozzle configuration based on the starting and + ending nozzle. + """ + if physical_rows == current_rows and physical_cols == current_cols: + return NozzleConfigurationType.FULL + if len(current_rows) == 1 and len(current_cols) == 1: + return NozzleConfigurationType.SINGLE + if len(current_rows) == 1: + return NozzleConfigurationType.ROW + if len(current_cols) == 1: + return NozzleConfigurationType.COLUMN + return NozzleConfigurationType.SUBRECT + + +@dataclass +class NozzleMap: + """ + A NozzleMap instance represents a specific configuration of active nozzles on a pipette. + + It exposes properties of the configuration like the configuration's front-right, front-left, + back-left and starting nozzles as well as a map of all the nozzles active in the configuration. + + Because NozzleMaps represent configurations directly, the properties of the NozzleMap may not + match the properties of the physical pipette. For instance, a NozzleMap for a single channel + configuration of an 8-channel pipette - say, A1 only - will have its front left, front right, + and active channels all be A1, while the physical configuration would have the front right + channel be H1. + """ + + starting_nozzle: str + #: The nozzle that automated operations that count nozzles should start at + # these are really ordered dicts but you can't say that even in quotes because pydantic needs to + # evaluate them to generate serdes code so please only use ordered dicts here + map_store: Dict[str, Point] + #: A map of all of the nozzles active in this configuration + rows: Dict[str, List[str]] + #: A map of all the rows active in this configuration + columns: Dict[str, List[str]] + #: A map of all the columns active in this configuration + configuration: NozzleConfigurationType + #: The kind of configuration this is + + def __str__(self) -> str: + return f"back_left_nozzle: {self.back_left} front_right_nozzle: {self.front_right} configuration: {self.configuration}" + + @property + def back_left(self) -> str: + """The backest, leftest (i.e. back if it's a column, left if it's a row) nozzle of the configuration. + + Note: This is the value relevant for this particular configuration, and it may not represent the back left nozzle + of the underlying physical pipette. For instance, the back-left nozzle of a configuration representing nozzles + D7 to H12 of a 96-channel pipette is D7, which is not the back-left nozzle of the physical pipette (A1). + """ + return next(iter(self.rows.values()))[0] + + @property + def front_right(self) -> str: + """The frontest, rightest (i.e. front if it's a column, right if it's a row) nozzle of the configuration. + + Note: This is the value relevant for this configuration, not the physical pipette. See the note on back_left. + """ + return next(reversed(list(self.rows.values())))[-1] + + @property + def starting_nozzle_offset(self) -> Point: + """The position of the starting nozzle.""" + return self.map_store[self.starting_nozzle] + + @property + def xy_center_offset(self) -> Point: + """The position of the geometrical center of all nozzles in the configuration. + + Note: This is the value relevant fro this configuration, not the physical pipette. See the note on back_left. + """ + difference = self.map_store[self.front_right] - self.map_store[self.back_left] + return self.map_store[self.back_left] + Point( + difference[0] / 2, difference[1] / 2, 0 + ) + + @property + def front_nozzle_offset(self) -> Point: + """The offset for the front_left nozzle.""" + # front left-most nozzle of the 96 channel in a given configuration + # and front nozzle of the 8 channel + front_left = next(iter(self.columns.values()))[-1] + return self.map_store[front_left] + + @property + def tip_count(self) -> int: + """The total number of active nozzles in the configuration, and thus the number of tips that will be picked up.""" + return len(self.map_store) + + @classmethod + def build( + cls, + physical_nozzles: "OrderedDict[str, Point]", + physical_rows: "OrderedDict[str, List[str]]", + physical_columns: "OrderedDict[str, List[str]]", + starting_nozzle: str, + back_left_nozzle: str, + front_right_nozzle: str, + ) -> "NozzleMap": + try: + back_left_row_index, back_left_column_index = _row_col_indices_for_nozzle( + physical_rows, physical_columns, back_left_nozzle + ) + except KeyError as e: + raise IncompatibleNozzleConfiguration( + message=f"No entry for back left nozzle {e} in pipette", + wrapping=[PythonException(e)], + ) from e + try: + ( + front_right_row_index, + front_right_column_index, + ) = _row_col_indices_for_nozzle( + physical_rows, physical_columns, front_right_nozzle + ) + except KeyError as e: + raise IncompatibleNozzleConfiguration( + message=f"No entry for front right nozzle {e} in pipette", + wrapping=[PythonException(e)], + ) from e + + correct_rows_with_all_columns = list(physical_rows.items())[ + back_left_row_index : front_right_row_index + 1 + ] + correct_rows = [ + ( + row_name, + row_entries[back_left_column_index : front_right_column_index + 1], + ) + for row_name, row_entries in correct_rows_with_all_columns + ] + rows = OrderedDict(correct_rows) + correct_columns_with_all_rows = list(physical_columns.items())[ + back_left_column_index : front_right_column_index + 1 + ] + correct_columns = [ + (col_name, col_entries[back_left_row_index : front_right_row_index + 1]) + for col_name, col_entries in correct_columns_with_all_rows + ] + columns = OrderedDict(correct_columns) + + map_store = OrderedDict( + (nozzle, physical_nozzles[nozzle]) for nozzle in chain(*rows.values()) + ) + + return cls( + starting_nozzle=starting_nozzle, + map_store=map_store, + rows=rows, + columns=columns, + configuration=NozzleConfigurationType.determine_nozzle_configuration( + physical_rows, rows, physical_columns, columns + ), + ) + + +class IncompatibleNozzleConfiguration(GeneralError): + """Error raised if nozzle configuration is incompatible with the currently loaded pipette.""" + + def __init__( + self, + message: Optional[str] = None, + detail: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[GeneralError]] = None, + ) -> None: + """Build a IncompatibleNozzleConfiguration error.""" + super().__init__( + code=ErrorCodes.API_MISCONFIGURATION, + message=message, + detail=detail, + wrapping=wrapping, + ) + + +class NozzleConfigurationManager: + def __init__( + self, + nozzle_map: NozzleMap, + ) -> None: + self._physical_nozzle_map = nozzle_map + self._current_nozzle_configuration = nozzle_map + + @classmethod + def build_from_config( + cls, pipette_geometry: PipetteGeometryDefinition + ) -> "NozzleConfigurationManager": + sorted_nozzle_map = OrderedDict( + ( + (k, Point(*pipette_geometry.nozzle_map[k])) + for k in _nozzle_names_by_row(pipette_geometry.ordered_rows) + ) + ) + sorted_rows = OrderedDict( + (entry.key, entry.ordered_nozzles) + for entry in pipette_geometry.ordered_rows + ) + sorted_cols = OrderedDict( + (entry.key, entry.ordered_nozzles) + for entry in pipette_geometry.ordered_columns + ) + back_left = next(iter(sorted_rows.values()))[0] + front_right = next(reversed(list(sorted_rows.values())))[-1] + starting_nozzle_config = NozzleMap.build( + physical_nozzles=sorted_nozzle_map, + physical_rows=sorted_rows, + physical_columns=sorted_cols, + starting_nozzle=back_left, + back_left_nozzle=back_left, + front_right_nozzle=front_right, + ) + return cls(starting_nozzle_config) + + @property + def starting_nozzle_offset(self) -> Point: + return self._current_nozzle_configuration.starting_nozzle_offset + + @property + def current_configuration(self) -> NozzleMap: + return self._current_nozzle_configuration + + def reset_to_default_configuration(self) -> None: + self._current_nozzle_configuration = self._physical_nozzle_map + + def update_nozzle_configuration( + self, + back_left_nozzle: str, + front_right_nozzle: str, + starting_nozzle: Optional[str] = None, + ) -> None: + self._current_nozzle_configuration = NozzleMap.build( + # these casts are because of pydantic in the protocol engine (see above) + physical_nozzles=cast( + "OrderedDict[str, Point]", self._physical_nozzle_map.map_store + ), + physical_rows=cast( + "OrderedDict[str, List[str]]", self._physical_nozzle_map.rows + ), + physical_columns=cast( + "OrderedDict[str, List[str]]", self._physical_nozzle_map.columns + ), + starting_nozzle=starting_nozzle or back_left_nozzle, + back_left_nozzle=back_left_nozzle, + front_right_nozzle=front_right_nozzle, + ) + + def get_tip_count(self) -> int: + return self._current_nozzle_configuration.tip_count + + def critical_point_with_tip_length( + self, + cp_override: Optional[CriticalPoint], + tip_length: float = 0.0, + ) -> Point: + if cp_override == CriticalPoint.XY_CENTER: + current_nozzle = self._current_nozzle_configuration.xy_center_offset + elif cp_override == CriticalPoint.FRONT_NOZZLE: + current_nozzle = self._current_nozzle_configuration.front_nozzle_offset + else: + current_nozzle = self.starting_nozzle_offset + return current_nozzle - Point(0, 0, tip_length) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index d106308055c..8a596f24e5e 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -54,7 +54,7 @@ MoveTarget, ZeroLengthMoveError, ) - +from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType from opentrons_hardware.hardware_control.motion import MoveStopCondition from opentrons_shared_data.errors.exceptions import ( EnumeratedError, @@ -945,7 +945,7 @@ async def current_position_ot3( raise PositionUnknownError( message=f"Motor positions for {str(mount)} mount are missing (" f"{mount_axes}); must first home motors.", - detail={"mount": str(mount), "missing_axes": mount_axes}, + detail={"mount": str(mount), "missing_axes": str(mount_axes)}, ) self._assert_motor_ok(mount_axes) @@ -1397,9 +1397,9 @@ async def _retrieve_home_position( self, axis: Axis ) -> Tuple[OT3AxisMap[float], OT3AxisMap[float]]: origin = await self._backend.update_position() - target_pos = {ax: pos for ax, pos in origin.items()} - target_pos.update({axis: self._backend.home_position()[axis]}) - return origin, target_pos + origin_pos = {axis: origin[axis]} + target_pos = {axis: self._backend.home_position()[axis]} + return origin_pos, target_pos @_adjust_high_throughput_z_current async def _home_axis(self, axis: Axis) -> None: @@ -2044,9 +2044,14 @@ def add_tip_to_instr() -> None: instrument.set_current_volume(0) await self._move_to_plunger_bottom(realmount, rate=1.0) - - if self.gantry_load == GantryLoad.HIGH_THROUGHPUT: - spec = self._pipette_handler.plan_ht_pick_up_tip() + if ( + self.gantry_load == GantryLoad.HIGH_THROUGHPUT + and instrument.nozzle_manager.current_configuration.configuration + == NozzleConfigurationType.FULL + ): + spec = self._pipette_handler.plan_ht_pick_up_tip( + instrument.nozzle_manager.current_configuration.tip_count + ) if spec.z_distance_to_tiprack: await self.move_rel( realmount, top_types.Point(z=spec.z_distance_to_tiprack) @@ -2054,7 +2059,10 @@ def add_tip_to_instr() -> None: await self._tip_motor_action(realmount, spec.tip_action_moves) else: spec = self._pipette_handler.plan_lt_pick_up_tip( - realmount, presses, increment + realmount, + instrument.nozzle_manager.current_configuration.tip_count, + presses, + increment, ) await self._force_pick_up_tip(realmount, spec) @@ -2365,6 +2373,41 @@ def get_instrument_max_height( return pos_at_home[Axis.by_mount(mount)] - self._config.z_retract_distance + async def update_nozzle_configuration_for_mount( + self, + mount: Union[top_types.Mount, OT3Mount], + back_left_nozzle: Optional[str] = None, + front_right_nozzle: Optional[str] = None, + starting_nozzle: Optional[str] = None, + ) -> None: + """ + The expectation of this function is that the back_left_nozzle/front_right_nozzle are the two corners + of a rectangle of nozzles. A call to this function that does not follow that schema will result + in an error. + + :param mount: A robot mount that the instrument is on. + :param back_left_nozzle: A string representing a nozzle name of the form such as 'A1'. + :param front_right_nozzle: A string representing a nozzle name of the form such as 'A1'. + :param starting_nozzle: A string representing the starting nozzle which will be used as the critical point + of the pipette nozzle configuration. By default, the back left nozzle will be the starting nozzle if + none is provided. + :return: None. + + If none of the nozzle parameters are provided, the nozzle configuration will be reset to default. + """ + if not back_left_nozzle and not front_right_nozzle and not starting_nozzle: + await self._pipette_handler.reset_nozzle_configuration( + OT3Mount.from_mount(mount) + ) + else: + assert back_left_nozzle and front_right_nozzle + await self._pipette_handler.update_nozzle_configuration( + OT3Mount.from_mount(mount), + back_left_nozzle, + front_right_nozzle, + starting_nozzle, + ) + async def add_tip( self, mount: Union[top_types.Mount, OT3Mount], tip_length: float ) -> None: diff --git a/api/src/opentrons/hardware_control/protocols/liquid_handler.py b/api/src/opentrons/hardware_control/protocols/liquid_handler.py index e25a25299ce..e46cea2fdc2 100644 --- a/api/src/opentrons/hardware_control/protocols/liquid_handler.py +++ b/api/src/opentrons/hardware_control/protocols/liquid_handler.py @@ -16,6 +16,30 @@ class LiquidHandler( Calibratable[CalibrationType], Protocol[CalibrationType], ): + async def update_nozzle_configuration_for_mount( + self, + mount: Mount, + back_left_nozzle: Optional[str], + front_right_nozzle: Optional[str], + starting_nozzle: Optional[str] = None, + ) -> None: + """ + The expectation of this function is that the back_left_nozzle/front_right_nozzle are the two corners + of a rectangle of nozzles. A call to this function that does not follow that schema will result + in an error. + + :param mount: A robot mount that the instrument is on. + :param back_left_nozzle: A string representing a nozzle name of the form such as 'A1'. + :param front_right_nozzle: A string representing a nozzle name of the form such as 'A1'. + :param starting_nozzle: A string representing the starting nozzle which will be used as the critical point + of the pipette nozzle configuration. By default, the back left nozzle will be the starting nozzle if + none is provided. + + If none of the nozzle parameters are provided, the nozzle configuration will be reset to default. + :return: None. + """ + ... + async def configure_for_volume(self, mount: Mount, volume: float) -> None: """ Configure a pipette to handle the specified volume. diff --git a/api/src/opentrons/protocol_api/__init__.py b/api/src/opentrons/protocol_api/__init__.py index c96fefba360..0d518bbf5c0 100644 --- a/api/src/opentrons/protocol_api/__init__.py +++ b/api/src/opentrons/protocol_api/__init__.py @@ -25,6 +25,10 @@ from ._liquid import Liquid from ._types import OFF_DECK from ._waste_chute import WasteChute +from ._nozzle_layout import ( + COLUMN, + EMPTY, +) from .create_protocol_context import ( create_protocol_context, @@ -48,6 +52,8 @@ "WasteChute", "Well", "Liquid", + "COLUMN", + "EMPTY", "OFF_DECK", # For internal Opentrons use only: "create_protocol_context", diff --git a/api/src/opentrons/protocol_api/_nozzle_layout.py b/api/src/opentrons/protocol_api/_nozzle_layout.py new file mode 100644 index 00000000000..45cabb24af6 --- /dev/null +++ b/api/src/opentrons/protocol_api/_nozzle_layout.py @@ -0,0 +1,27 @@ +from typing_extensions import Final +import enum + + +class NozzleLayout(enum.Enum): + COLUMN = "COLUMN" + SINGLE = "SINGLE" + ROW = "ROW" + QUADRANT = "QUADRANT" + EMPTY = "EMPTY" + + +COLUMN: Final = NozzleLayout.COLUMN +EMPTY: Final = NozzleLayout.EMPTY + +# Set __doc__ manually as a workaround. When this docstring is written the normal way, right after +# the constant definition, Sphinx has trouble picking it up. +COLUMN.__doc__ = """\ +A special nozzle configuration type indicating a full single column pick up. Predominantly meant for the 96 channel. + +See for details on using ``COLUMN`` with :py:obj:`InstrumentContext.configure_nozzle_layout()`. +""" +EMPTY.__doc__ = """\ +A special nozzle configuration type indicating a reset back to default where the pipette will pick up its max capacity of tips. + +See for details on using ``RESET`` with :py:obj:`InstrumentContext.configure_nozzle_layout()`. +""" diff --git a/api/src/opentrons/protocol_api/_types.py b/api/src/opentrons/protocol_api/_types.py index dea183c2eab..067401cfc8e 100644 --- a/api/src/opentrons/protocol_api/_types.py +++ b/api/src/opentrons/protocol_api/_types.py @@ -1,3 +1,4 @@ +from __future__ import annotations from typing_extensions import Final import enum @@ -16,3 +17,35 @@ class OffDeckType(enum.Enum): See :ref:`off-deck-location` for details on using ``OFF_DECK`` with :py:obj:`ProtocolContext.move_labware()`. """ + + +# TODO(jbl 11-17-2023) move this away from being an Enum and make this a NewType or something similar +class StagingSlotName(enum.Enum): + """Staging slot identifiers.""" + + SLOT_A4 = "A4" + SLOT_B4 = "B4" + SLOT_C4 = "C4" + SLOT_D4 = "D4" + + @classmethod + def from_primitive(cls, value: str) -> StagingSlotName: + str_val = value.upper() + return cls(str_val) + + @property + def id(self) -> str: + """This slot's unique ID, as it appears in the deck definition. + + This can be used to look up slot details in the deck definition. + + This is preferred over `.value` or `.__str__()` for explicitness. + """ + return self.value + + def __str__(self) -> str: + """Stringify to the unique ID. + + For explicitness, prefer using `.id` instead. + """ + return self.id diff --git a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py index 7314d8074cd..cd1c892c953 100644 --- a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py +++ b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py @@ -10,6 +10,7 @@ DeckSlotLocation, ModuleLocation, OnLabwareLocation, + AddressableAreaLocation, OFF_DECK_LOCATION, ) from opentrons.protocol_engine.errors.exceptions import LabwareNotLoadedOnModuleError @@ -111,6 +112,16 @@ def _map_labware( ) -> Optional[Tuple[DeckSlotName, wrapped_deck_conflict.DeckItem]]: location_from_engine = engine_state.labware.get_location(labware_id=labware_id) + if isinstance(location_from_engine, AddressableAreaLocation): + # TODO need to deal with staging slots, which will raise the value error we are returning None with below + try: + deck_slot = DeckSlotName.from_primitive( + location_from_engine.addressableAreaName + ) + except ValueError: + return None + location_from_engine = DeckSlotLocation(slotName=deck_slot) + if isinstance(location_from_engine, DeckSlotLocation): # This labware is loaded directly into a deck slot. # Map it to a wrapped_deck_conflict.Labware. diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 79773c98264..f05b3c9eeda 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -1,7 +1,7 @@ """ProtocolEngine-based InstrumentContext core implementation.""" from __future__ import annotations -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, cast from opentrons.types import Location, Mount from opentrons.hardware_control import SyncHardwareAPI @@ -14,6 +14,15 @@ WellLocation, WellOrigin, WellOffset, + EmptyNozzleLayoutConfiguration, + SingleNozzleLayoutConfiguration, + RowNozzleLayoutConfiguration, + ColumnNozzleLayoutConfiguration, + QuadrantNozzleLayoutConfiguration, +) +from opentrons.protocol_engine.types import ( + PRIMARY_NOZZLE_LITERAL, + NozzleLayoutConfigurationType, ) from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError from opentrons.protocol_engine.clients import SyncClient as EngineClient @@ -21,6 +30,8 @@ from opentrons.types import Point, DeckSlotName from opentrons_shared_data.pipette.dev_types import PipetteNameType +from opentrons.protocol_api._nozzle_layout import NozzleLayout +from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType from ..instrument import AbstractInstrument from .well import WellCore @@ -564,6 +575,11 @@ def get_dispense_flow_rate(self, rate: float = 1.0) -> float: def get_blow_out_flow_rate(self, rate: float = 1.0) -> float: return self._blow_out_flow_rate * rate + def get_nozzle_configuration(self) -> NozzleConfigurationType: + return self._engine_client.state.pipettes.get_nozzle_layout_type( + self._pipette_id + ) + def set_flow_rate( self, aspirate: Optional[float] = None, @@ -587,3 +603,36 @@ def configure_for_volume(self, volume: float) -> None: def prepare_to_aspirate(self) -> None: self._engine_client.prepare_to_aspirate(pipette_id=self._pipette_id) + + def configure_nozzle_layout( + self, + style: NozzleLayout, + primary_nozzle: Optional[str], + front_right_nozzle: Optional[str], + ) -> None: + + if style == NozzleLayout.COLUMN: + configuration_model: NozzleLayoutConfigurationType = ( + ColumnNozzleLayoutConfiguration( + primary_nozzle=cast(PRIMARY_NOZZLE_LITERAL, primary_nozzle) + ) + ) + elif style == NozzleLayout.ROW: + configuration_model = RowNozzleLayoutConfiguration( + primary_nozzle=cast(PRIMARY_NOZZLE_LITERAL, primary_nozzle) + ) + elif style == NozzleLayout.QUADRANT: + assert front_right_nozzle is not None + configuration_model = QuadrantNozzleLayoutConfiguration( + primary_nozzle=cast(PRIMARY_NOZZLE_LITERAL, primary_nozzle), + front_right_nozzle=front_right_nozzle, + ) + elif style == NozzleLayout.SINGLE: + configuration_model = SingleNozzleLayoutConfiguration( + primary_nozzle=cast(PRIMARY_NOZZLE_LITERAL, primary_nozzle) + ) + else: + configuration_model = EmptyNozzleLayoutConfiguration() + self._engine_client.configure_nozzle_layout( + pipette_id=self._pipette_id, configuration_params=configuration_model + ) diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index 67e588d3a10..859eaee3636 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -20,6 +20,7 @@ from opentrons.protocol_engine import ( DeckSlotLocation, + AddressableAreaLocation, ModuleLocation, OnLabwareLocation, ModuleModel as EngineModuleModel, @@ -41,7 +42,7 @@ ) from ... import validation -from ..._types import OffDeckType, OFF_DECK +from ..._types import OffDeckType, OFF_DECK, StagingSlotName from ..._liquid import Liquid from ..._waste_chute import WasteChute from ..protocol import AbstractProtocol @@ -143,7 +144,12 @@ def load_labware( self, load_name: str, location: Union[ - DeckSlotName, LabwareCore, ModuleCore, NonConnectedModuleCore, OffDeckType + DeckSlotName, + StagingSlotName, + LabwareCore, + ModuleCore, + NonConnectedModuleCore, + OffDeckType, ], label: Optional[str], namespace: Optional[str], @@ -204,7 +210,13 @@ def load_labware( def load_adapter( self, load_name: str, - location: Union[DeckSlotName, ModuleCore, NonConnectedModuleCore, OffDeckType], + location: Union[ + DeckSlotName, + StagingSlotName, + ModuleCore, + NonConnectedModuleCore, + OffDeckType, + ], namespace: Optional[str], version: Optional[int], ) -> LabwareCore: @@ -255,6 +267,7 @@ def move_labware( labware_core: LabwareCore, new_location: Union[ DeckSlotName, + StagingSlotName, LabwareCore, ModuleCore, NonConnectedModuleCore, @@ -541,7 +554,7 @@ def get_deck_definition(self) -> DeckDefinitionV4: def get_slot_definition(self, slot: DeckSlotName) -> SlotDefV3: """Get the slot definition from the robot's deck.""" - return self._engine_client.state.labware.get_slot_definition(slot) + return self._engine_client.state.addressable_areas.get_slot_definition(slot) def _ensure_module_location( self, slot: DeckSlotName, module_type: ModuleType @@ -593,7 +606,9 @@ def get_labware_on_labware( def get_slot_center(self, slot_name: DeckSlotName) -> Point: """Get the absolute coordinate of a slot's center.""" - return self._engine_client.state.labware.get_slot_center_position(slot_name) + return self._engine_client.state.addressable_areas.get_addressable_area_center( + slot_name.id + ) def get_highest_z(self) -> float: """Get the highest Z point of all deck items.""" @@ -648,7 +663,12 @@ def get_labware_location( def _convert_labware_location( self, location: Union[ - DeckSlotName, LabwareCore, ModuleCore, NonConnectedModuleCore, OffDeckType + DeckSlotName, + StagingSlotName, + LabwareCore, + ModuleCore, + NonConnectedModuleCore, + OffDeckType, ], ) -> LabwareLocation: if isinstance(location, LabwareCore): @@ -658,7 +678,13 @@ def _convert_labware_location( @staticmethod def _get_non_stacked_location( - location: Union[DeckSlotName, ModuleCore, NonConnectedModuleCore, OffDeckType] + location: Union[ + DeckSlotName, + StagingSlotName, + ModuleCore, + NonConnectedModuleCore, + OffDeckType, + ] ) -> NonStackedLocation: if isinstance(location, (ModuleCore, NonConnectedModuleCore)): return ModuleLocation(moduleId=location.module_id) @@ -666,3 +692,5 @@ def _get_non_stacked_location( return OFF_DECK_LOCATION elif isinstance(location, DeckSlotName): return DeckSlotLocation(slotName=location) + elif isinstance(location, StagingSlotName): + return AddressableAreaLocation(addressableAreaName=location.id) diff --git a/api/src/opentrons/protocol_api/core/engine/stringify.py b/api/src/opentrons/protocol_api/core/engine/stringify.py index fd4a90817cd..434dde1b08a 100644 --- a/api/src/opentrons/protocol_api/core/engine/stringify.py +++ b/api/src/opentrons/protocol_api/core/engine/stringify.py @@ -4,6 +4,7 @@ LabwareLocation, ModuleLocation, OnLabwareLocation, + AddressableAreaLocation, ) @@ -42,6 +43,10 @@ def _labware_location_string( labware_on_string = _labware_location_string(engine_client, labware_on) return f"{labware_name} on {labware_on_string}" + elif isinstance(location, AddressableAreaLocation): + # In practice this will always be a deck slot or staging slot + return f"slot {location.addressableAreaName}" + elif location == "offDeck": return "[off-deck]" diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index bcec7f9c0f6..6429e253c2e 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -8,6 +8,7 @@ from opentrons import types from opentrons.hardware_control.dev_types import PipetteDict from opentrons.protocols.api_support.util import FlowRates +from opentrons.protocol_api._nozzle_layout import NozzleLayout from .._waste_chute import WasteChute from .well import WellCoreType @@ -247,5 +248,20 @@ def prepare_to_aspirate(self) -> None: """Prepare the pipette to aspirate.""" ... + def configure_nozzle_layout( + self, + style: NozzleLayout, + primary_nozzle: Optional[str], + front_right_nozzle: Optional[str], + ) -> None: + """Configure the pipette to a specific nozzle layout. + + Args: + style: The type of configuration you wish to build. + primary_nozzle: The nozzle that will determine a pipettes critical point. + front_right_nozzle: The front right most nozzle in the requested layout. + """ + ... + InstrumentCoreType = TypeVar("InstrumentCoreType", bound=AbstractInstrument[Any]) diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index b9e661a9fce..5ce5fd595c9 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -17,6 +17,7 @@ APIVersionError, ) from opentrons.protocols.geometry import planning +from opentrons.protocol_api._nozzle_layout import NozzleLayout from ..._waste_chute import WasteChute from ..instrument import AbstractInstrument @@ -519,3 +520,12 @@ def flag_unsafe_move(self, location: types.Location) -> None: def configure_for_volume(self, volume: float) -> None: """This will never be called because it was added in API 2.15.""" pass + + def configure_nozzle_layout( + self, + style: NozzleLayout, + primary_nozzle: Optional[str], + front_right_nozzle: Optional[str], + ) -> None: + """This will never be called because it was added in API 2.15.""" + pass diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py index 01faa63c17b..3b74c3b8051 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_protocol_core.py @@ -17,7 +17,7 @@ from ...labware import Labware from ..._liquid import Liquid -from ..._types import OffDeckType +from ..._types import OffDeckType, StagingSlotName from ..._waste_chute import WasteChute from ..protocol import AbstractProtocol from ..labware import LabwareLoadParams @@ -152,6 +152,7 @@ def load_labware( DeckSlotName, LegacyLabwareCore, legacy_module_core.LegacyModuleCore, + StagingSlotName, OffDeckType, ], label: Optional[str], @@ -167,6 +168,8 @@ def load_labware( raise APIVersionError( "Loading a labware onto another labware or adapter is only supported with api version 2.15 and above" ) + elif isinstance(location, StagingSlotName): + raise APIVersionError("Using a staging deck slot requires apiLevel 2.16.") deck_slot = ( location if isinstance(location, DeckSlotName) else location.get_deck_slot() @@ -237,7 +240,12 @@ def load_labware( def load_adapter( self, load_name: str, - location: Union[DeckSlotName, legacy_module_core.LegacyModuleCore, OffDeckType], + location: Union[ + DeckSlotName, + StagingSlotName, + legacy_module_core.LegacyModuleCore, + OffDeckType, + ], namespace: Optional[str], version: Optional[int], ) -> LegacyLabwareCore: @@ -250,6 +258,7 @@ def move_labware( labware_core: LegacyLabwareCore, new_location: Union[ DeckSlotName, + StagingSlotName, LegacyLabwareCore, legacy_module_core.LegacyModuleCore, OffDeckType, diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index ab90676c27e..27ed2a5d438 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -22,6 +22,7 @@ ) from ..._waste_chute import WasteChute +from opentrons.protocol_api._nozzle_layout import NozzleLayout from ..instrument import AbstractInstrument @@ -435,3 +436,12 @@ def _raise_if_tip(self, action: str) -> None: def configure_for_volume(self, volume: float) -> None: """This will never be called because it was added in API 2.15.""" pass + + def configure_nozzle_layout( + self, + style: NozzleLayout, + primary_nozzle: Optional[str], + front_right_nozzle: Optional[str], + ) -> None: + """This will never be called because it was added in API 2.15.""" + pass diff --git a/api/src/opentrons/protocol_api/core/protocol.py b/api/src/opentrons/protocol_api/core/protocol.py index 596cb9c6da4..521e719d228 100644 --- a/api/src/opentrons/protocol_api/core/protocol.py +++ b/api/src/opentrons/protocol_api/core/protocol.py @@ -20,7 +20,7 @@ from .module import ModuleCoreType from .._liquid import Liquid from .._waste_chute import WasteChute -from .._types import OffDeckType +from .._types import OffDeckType, StagingSlotName class AbstractProtocol( @@ -61,7 +61,9 @@ def add_labware_definition( def load_labware( self, load_name: str, - location: Union[DeckSlotName, LabwareCoreType, ModuleCoreType, OffDeckType], + location: Union[ + DeckSlotName, StagingSlotName, LabwareCoreType, ModuleCoreType, OffDeckType + ], label: Optional[str], namespace: Optional[str], version: Optional[int], @@ -73,7 +75,7 @@ def load_labware( def load_adapter( self, load_name: str, - location: Union[DeckSlotName, ModuleCoreType, OffDeckType], + location: Union[DeckSlotName, StagingSlotName, ModuleCoreType, OffDeckType], namespace: Optional[str], version: Optional[int], ) -> LabwareCoreType: @@ -86,7 +88,12 @@ def move_labware( self, labware_core: LabwareCoreType, new_location: Union[ - DeckSlotName, LabwareCoreType, ModuleCoreType, OffDeckType, WasteChute + DeckSlotName, + StagingSlotName, + LabwareCoreType, + ModuleCoreType, + OffDeckType, + WasteChute, ], use_gripper: bool, pause_for_manual_move: bool, diff --git a/api/src/opentrons/protocol_api/deck.py b/api/src/opentrons/protocol_api/deck.py index c5c9fcb2368..1b72e5f3013 100644 --- a/api/src/opentrons/protocol_api/deck.py +++ b/api/src/opentrons/protocol_api/deck.py @@ -42,9 +42,12 @@ def _get_slot_name( slot_key: DeckLocation, api_version: APIVersion, robot_type: RobotType ) -> DeckSlotName: try: - return validation.ensure_and_convert_deck_slot( + slot = validation.ensure_and_convert_deck_slot( slot_key, api_version, robot_type ) + if not isinstance(slot, DeckSlotName): + raise ValueError("Cannot currently load staging slots from Deck.") + return slot except (TypeError, ValueError) as error: raise KeyError(slot_key) from error diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 9f03645f69d..98975147112 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -2,7 +2,7 @@ import logging from contextlib import nullcontext -from typing import Any, List, Optional, Sequence, Union, cast +from typing import Any, List, Optional, Sequence, Union, cast, Dict from opentrons_shared_data.errors.exceptions import ( CommandPreconditionViolated, CommandParameterLimitViolated, @@ -33,6 +33,7 @@ from .core.legacy.legacy_instrument_core import LegacyInstrumentCore from .config import Clearances from ._waste_chute import WasteChute +from ._nozzle_layout import NozzleLayout from . import labware, validation @@ -164,9 +165,15 @@ def aspirate( See :ref:`new-aspirate` for more details and examples. - :param volume: The volume to aspirate, measured in µL. If 0 or unspecified, + :param volume: The volume to aspirate, measured in µL. If unspecified, defaults to the maximum volume for the pipette and its currently attached tip. + + If ``aspirate`` is called with a volume of precisely 0, its behavior + depends on the API level of the protocol. On API levels below 2.16, + it will behave the same as a volume of ``None``/unspecified: aspirate + until the pipette is full. On API levels at or above 2.16, no liquid + will be aspirated. :type volume: int or float :param location: Tells the robot where to aspirate from. The location can be a :py:class:`.Well` or a :py:class:`.Location`. @@ -235,7 +242,10 @@ def aspirate( reject_adapter=self.api_version >= APIVersion(2, 15), ) - c_vol = self._core.get_available_volume() if not volume else volume + if self.api_version >= APIVersion(2, 16): + c_vol = self._core.get_available_volume() if volume is None else volume + else: + c_vol = self._core.get_available_volume() if not volume else volume flow_rate = self._core.get_aspirate_flow_rate(rate) with publisher.publish_context( @@ -259,7 +269,7 @@ def aspirate( return self - @requires_version(2, 0) + @requires_version(2, 0) # noqa: C901 def dispense( self, volume: Optional[float] = None, @@ -272,9 +282,15 @@ def dispense( See :ref:`new-dispense` for more details and examples. - :param volume: The volume to dispense, measured in µL. If 0 or unspecified, + :param volume: The volume to dispense, measured in µL. If unspecified, defaults to :py:attr:`current_volume`. If only a volume is passed, the pipette will dispense from its current position. + + If ``dispense`` is called with a volume of precisely 0, its behavior + depends on the API level of the protocol. On API levels below 2.16, + it will behave the same as a volume of ``None``/unspecified: dispense + all liquid in the pipette. On API levels at or above 2.16, no liquid + will be dispensed. :type volume: int or float :param location: Tells the robot where to dispense liquid held in the pipette. @@ -362,7 +378,10 @@ def dispense( reject_adapter=self.api_version >= APIVersion(2, 15), ) - c_vol = self._core.get_current_volume() if not volume else volume + if self.api_version >= APIVersion(2, 16): + c_vol = self._core.get_current_volume() if volume is None else volume + else: + c_vol = self._core.get_current_volume() if not volume else volume flow_rate = self._core.get_dispense_flow_rate(rate) @@ -402,8 +421,14 @@ def mix( See :ref:`mix` for examples. :param repetitions: Number of times to mix (default is 1). - :param volume: The volume to mix, measured in µL. If 0 or unspecified, defaults + :param volume: The volume to mix, measured in µL. If unspecified, defaults to the maximum volume for the pipette and its attached tip. + + If ``mix`` is called with a volume of precisely 0, its behavior + depends on the API level of the protocol. On API levels below 2.16, + it will behave the same as a volume of ``None``/unspecified: mix + the full working volume of the pipette. On API levels at or above 2.16, + no liquid will be mixed. :param location: The :py:class:`.Well` or :py:class:`~.types.Location` where the pipette will mix. If unspecified, the pipette will mix at its current position. @@ -432,7 +457,14 @@ def mix( if not self._core.has_tip(): raise UnexpectedTipRemovalError("mix", self.name, self.mount) - c_vol = self._core.get_available_volume() if not volume else volume + if self.api_version >= APIVersion(2, 16): + c_vol = self._core.get_available_volume() if volume is None else volume + else: + c_vol = self._core.get_available_volume() if not volume else volume + + dispense_kwargs: Dict[str, Any] = {} + if self.api_version >= APIVersion(2, 16): + dispense_kwargs["push_out"] = 0.0 with publisher.publish_context( broker=self.broker, @@ -445,7 +477,7 @@ def mix( ): self.aspirate(volume, location, rate) while repetitions - 1 > 0: - self.dispense(volume, rate=rate) + self.dispense(volume, rate=rate, **dispense_kwargs) self.aspirate(volume, rate=rate) repetitions -= 1 self.dispense(volume, rate=rate) @@ -1658,3 +1690,57 @@ def prepare_to_aspirate(self) -> None: message=f"Cannot prepare {str(self)} for aspirate while it contains liquid." ) self._core.prepare_to_aspirate() + + def configure_nozzle_layout( + self, + style: NozzleLayout, + start: Optional[str] = None, + front_right: Optional[str] = None, + ) -> None: + """Configure a pipette to pick up less than the maximum tip capacity. The pipette + will remain in its partial state until this function is called again without any inputs. All subsequent + pipetting calls will execute with the new nozzle layout meaning that the pipette will perform + robot moves in the set nozzle layout. + + :param style: The requested nozzle layout should specify the shape that you + wish to configure your pipette to. Certain pipettes are restricted to a subset of `NozzleLayout` + types. See the note below on the different `NozzleLayout` types. + :type requested_nozzle_layout: `NozzleLayout.COLUMN`, `NozzleLayout.EMPTY` or None. + :param start: Signifies the nozzle that the robot will use to determine how to perform moves to different locations on the deck. + :type start: string or None. + :param front_right: Signifies the ending nozzle in your partial configuration. It is not required for NozzleLayout.COLUMN, NozzleLayout.ROW, or NozzleLayout.SINGLE + configurations. + :type front_right: string or None. + + .. note:: + Your `start` and `front_right` strings should be formatted similarly to a well, so in the format of . + The pipette nozzles are mapped in the same format as a 96 well standard plate starting from the back left-most nozzle + to the front right-most nozzle. + + .. code-block:: python + + from opentrons.protocol_api import COLUMN, EMPTY + + # Sets a pipette to a full single column pickup using "A1" as the primary nozzle. Implicitly, "H1" is the ending nozzle. + instr.configure_nozzle_layout(style=COLUMN, start="A1") + + # Resets the pipette configuration to default + instr.configure_nozzle_layout(style=EMPTY) + """ + if style != NozzleLayout.EMPTY: + if start is None: + raise ValueError( + f"Cannot configure a nozzle layout of style {style.value} without a starting nozzle." + ) + if start not in types.ALLOWED_PRIMARY_NOZZLES: + raise ValueError( + f"Starting nozzle specified is not of {types.ALLOWED_PRIMARY_NOZZLES}" + ) + if style == NozzleLayout.QUADRANT: + if front_right is None: + raise ValueError( + "Cannot configure a QUADRANT layout without a front right nozzle." + ) + self._core.configure_nozzle_layout( + style, primary_nozzle=start, front_right_nozzle=front_right + ) diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index ec1ff432384..be65d8e3020 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -31,7 +31,7 @@ APIVersionError, ) -from ._types import OffDeckType +from ._types import OffDeckType, StagingSlotName from .core.common import ModuleCore, LabwareCore, ProtocolCore from .core.core_map import LoadedCoreMap from .core.engine.module_core import NonConnectedModuleCore @@ -360,7 +360,7 @@ def load_labware( ) load_name = validation.ensure_lowercase_name(load_name) - load_location: Union[OffDeckType, DeckSlotName, LabwareCore] + load_location: Union[OffDeckType, DeckSlotName, StagingSlotName, LabwareCore] if adapter is not None: if self._api_version < APIVersion(2, 15): raise APIVersionError( @@ -493,7 +493,7 @@ def load_adapter( leave this unspecified to let the implementation choose a good default. """ load_name = validation.ensure_lowercase_name(load_name) - load_location: Union[OffDeckType, DeckSlotName] + load_location: Union[OffDeckType, DeckSlotName, StagingSlotName] if isinstance(location, OffDeckType): load_location = location else: @@ -599,7 +599,14 @@ def move_labware( f"Expected labware of type 'Labware' but got {type(labware)}." ) - location: Union[ModuleCore, LabwareCore, WasteChute, OffDeckType, DeckSlotName] + location: Union[ + ModuleCore, + LabwareCore, + WasteChute, + OffDeckType, + DeckSlotName, + StagingSlotName, + ] if isinstance(new_location, (Labware, ModuleContext)): location = new_location._core elif isinstance(new_location, (OffDeckType, WasteChute)): @@ -709,6 +716,8 @@ def load_module( location, self._api_version, self._core.robot_type ) ) + if isinstance(deck_slot, StagingSlotName): + raise ValueError("Cannot load a module onto a staging slot.") module_core = self._core.load_module( model=requested_model, diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index c6767ebc71f..54244c191ee 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -31,6 +31,7 @@ MagneticBlockModel, ThermocyclerStep, ) +from ._types import StagingSlotName if TYPE_CHECKING: from .labware import Well @@ -39,6 +40,9 @@ # The first APIVersion where Python protocols can specify deck labels like "D1" instead of "1". _COORDINATE_DECK_LABEL_VERSION_GATE = APIVersion(2, 15) +# The first APIVersion where Python protocols can specify staging deck slots (e.g. "D4") +_STAGING_DECK_SLOT_VERSION_GATE = APIVersion(2, 16) + # Mapping of public Python Protocol API pipette load names # to names used by the internal Opentrons system _PIPETTE_NAMES_MAP = { @@ -125,9 +129,12 @@ def ensure_pipette_name(pipette_name: str) -> PipetteNameType: ) from None +# TODO(jbl 11-17-2023) this function's original purpose was ensure a valid deck slot for a given robot type +# With deck configuration, the shape of this should change to better represent it checking if a deck slot +# (and maybe any addressable area) being valid for that deck configuration def ensure_and_convert_deck_slot( deck_slot: Union[int, str], api_version: APIVersion, robot_type: RobotType -) -> DeckSlotName: +) -> Union[DeckSlotName, StagingSlotName]: """Ensure that a primitive value matches a named deck slot. Also, convert the deck slot to match the given `robot_type`. @@ -149,21 +156,29 @@ def ensure_and_convert_deck_slot( if not isinstance(deck_slot, (int, str)): raise TypeError(f"Deck slot must be a string or integer, but got {deck_slot}") - try: - parsed_slot = DeckSlotName.from_primitive(deck_slot) - except ValueError as e: - raise ValueError(f"'{deck_slot}' is not a valid deck slot") from e - - is_ot2_style = parsed_slot.to_ot2_equivalent() == parsed_slot - if not is_ot2_style and api_version < _COORDINATE_DECK_LABEL_VERSION_GATE: - alternative = parsed_slot.to_ot2_equivalent().id - raise APIVersionError( - f'Specifying a deck slot like "{deck_slot}" requires apiLevel' - f" {_COORDINATE_DECK_LABEL_VERSION_GATE}." - f' Increase your protocol\'s apiLevel, or use slot "{alternative}" instead.' - ) + if str(deck_slot).upper() in {"A4", "B4", "C4", "D4"}: + if api_version < APIVersion(2, 16): + raise APIVersionError( + f"Using a staging deck slot requires apiLevel {_STAGING_DECK_SLOT_VERSION_GATE}." + ) + # Don't need a try/except since we're already pre-validating this + parsed_staging_slot = StagingSlotName.from_primitive(str(deck_slot)) + return parsed_staging_slot + else: + try: + parsed_slot = DeckSlotName.from_primitive(deck_slot) + except ValueError as e: + raise ValueError(f"'{deck_slot}' is not a valid deck slot") from e + is_ot2_style = parsed_slot.to_ot2_equivalent() == parsed_slot + if not is_ot2_style and api_version < _COORDINATE_DECK_LABEL_VERSION_GATE: + alternative = parsed_slot.to_ot2_equivalent().id + raise APIVersionError( + f'Specifying a deck slot like "{deck_slot}" requires apiLevel' + f" {_COORDINATE_DECK_LABEL_VERSION_GATE}." + f' Increase your protocol\'s apiLevel, or use slot "{alternative}" instead.' + ) - return parsed_slot.to_equivalent_for_robot_type(robot_type) + return parsed_slot.to_equivalent_for_robot_type(robot_type) def internal_slot_to_public_string( diff --git a/api/src/opentrons/protocol_engine/__init__.py b/api/src/opentrons/protocol_engine/__init__.py index a975b497332..79e5129f093 100644 --- a/api/src/opentrons/protocol_engine/__init__.py +++ b/api/src/opentrons/protocol_engine/__init__.py @@ -35,6 +35,7 @@ DeckSlotLocation, ModuleLocation, OnLabwareLocation, + AddressableAreaLocation, OFF_DECK_LOCATION, Dimensions, EngineStatus, @@ -52,6 +53,11 @@ ModuleModel, ModuleDefinition, Liquid, + EmptyNozzleLayoutConfiguration, + SingleNozzleLayoutConfiguration, + RowNozzleLayoutConfiguration, + ColumnNozzleLayoutConfiguration, + QuadrantNozzleLayoutConfiguration, ) @@ -88,6 +94,7 @@ "DeckType", "ModuleLocation", "OnLabwareLocation", + "AddressableAreaLocation", "OFF_DECK_LOCATION", "Dimensions", "EngineStatus", @@ -105,6 +112,11 @@ "ModuleModel", "ModuleDefinition", "Liquid", + "EmptyNozzleLayoutConfiguration", + "SingleNozzleLayoutConfiguration", + "RowNozzleLayoutConfiguration", + "ColumnNozzleLayoutConfiguration", + "QuadrantNozzleLayoutConfiguration", # plugins "AbstractPlugin", ] diff --git a/api/src/opentrons/protocol_engine/actions/actions.py b/api/src/opentrons/protocol_engine/actions/actions.py index 64e4a5a1fad..8dffc7f012c 100644 --- a/api/src/opentrons/protocol_engine/actions/actions.py +++ b/api/src/opentrons/protocol_engine/actions/actions.py @@ -15,7 +15,7 @@ from opentrons_shared_data.errors import EnumeratedError from ..commands import Command, CommandCreate, CommandPrivateResult -from ..types import LabwareOffsetCreate, ModuleDefinition, Liquid +from ..types import LabwareOffsetCreate, ModuleDefinition, Liquid, DeckConfigurationType @dataclass(frozen=True) @@ -23,6 +23,7 @@ class PlayAction: """Start or resume processing commands in the engine.""" requested_at: datetime + deck_configuration: Optional[DeckConfigurationType] class PauseSource(str, Enum): diff --git a/api/src/opentrons/protocol_engine/clients/sync_client.py b/api/src/opentrons/protocol_engine/clients/sync_client.py index cfef710a5ce..1982ad66fa1 100644 --- a/api/src/opentrons/protocol_engine/clients/sync_client.py +++ b/api/src/opentrons/protocol_engine/clients/sync_client.py @@ -23,6 +23,7 @@ LabwareOffsetVector, MotorAxis, Liquid, + NozzleLayoutConfigurationType, ) from .transports import ChildThreadTransport @@ -292,6 +293,20 @@ def prepare_to_aspirate(self, pipette_id: str) -> commands.PrepareToAspirateResu result = self._transport.execute_command(request=request) return cast(commands.PrepareToAspirateResult, result) + def configure_nozzle_layout( + self, + pipette_id: str, + configuration_params: NozzleLayoutConfigurationType, + ) -> commands.ConfigureNozzleLayoutResult: + """Execute a ConfigureForVolume command.""" + request = commands.ConfigureNozzleLayoutCreate( + params=commands.ConfigureNozzleLayoutParams( + pipetteId=pipette_id, configuration_params=configuration_params + ) + ) + result = self._transport.execute_command(request=request) + return cast(commands.ConfigureNozzleLayoutResult, result) + def aspirate( self, pipette_id: str, diff --git a/api/src/opentrons/protocol_engine/commands/__init__.py b/api/src/opentrons/protocol_engine/commands/__init__.py index 85b25046479..70aa90b3c7a 100644 --- a/api/src/opentrons/protocol_engine/commands/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/__init__.py @@ -177,6 +177,14 @@ MoveToWellCommandType, ) +from .move_to_addressable_area import ( + MoveToAddressableArea, + MoveToAddressableAreaParams, + MoveToAddressableAreaCreate, + MoveToAddressableAreaResult, + MoveToAddressableAreaCommandType, +) + from .wait_for_resume import ( WaitForResume, WaitForResumeParams, @@ -274,6 +282,14 @@ PrepareToAspirateCommandType, ) +from .configure_nozzle_layout import ( + ConfigureNozzleLayout, + ConfigureNozzleLayoutCreate, + ConfigureNozzleLayoutParams, + ConfigureNozzleLayoutResult, + ConfigureNozzleLayoutCommandType, +) + __all__ = [ # command type unions "Command", @@ -396,6 +412,12 @@ "MoveToWellParams", "MoveToWellResult", "MoveToWellCommandType", + # move to addressable area command models + "MoveToAddressableArea", + "MoveToAddressableAreaParams", + "MoveToAddressableAreaCreate", + "MoveToAddressableAreaResult", + "MoveToAddressableAreaCommandType", # wait for resume command models "WaitForResume", "WaitForResumeParams", @@ -477,4 +499,10 @@ "PrepareToAspirateParams", "PrepareToAspirateResult", "PrepareToAspirateCommandType", + # configure nozzle layout command bundle + "ConfigureNozzleLayout", + "ConfigureNozzleLayoutCreate", + "ConfigureNozzleLayoutParams", + "ConfigureNozzleLayoutResult", + "ConfigureNozzleLayoutCommandType", ] diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index bf9ff6b7768..464ed80f374 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -154,6 +154,14 @@ MoveToWellCommandType, ) +from .move_to_addressable_area import ( + MoveToAddressableArea, + MoveToAddressableAreaParams, + MoveToAddressableAreaCreate, + MoveToAddressableAreaResult, + MoveToAddressableAreaCommandType, +) + from .wait_for_resume import ( WaitForResume, WaitForResumeParams, @@ -243,6 +251,15 @@ PrepareToAspirateCommandType, ) +from .configure_nozzle_layout import ( + ConfigureNozzleLayout, + ConfigureNozzleLayoutCreate, + ConfigureNozzleLayoutParams, + ConfigureNozzleLayoutResult, + ConfigureNozzleLayoutCommandType, + ConfigureNozzleLayoutPrivateResult, +) + Command = Union[ Aspirate, AspirateInPlace, @@ -253,6 +270,7 @@ BlowOut, BlowOutInPlace, ConfigureForVolume, + ConfigureNozzleLayout, DropTip, DropTipInPlace, Home, @@ -265,6 +283,7 @@ MoveRelative, MoveToCoordinates, MoveToWell, + MoveToAddressableArea, PrepareToAspirate, WaitForResume, WaitForDuration, @@ -305,6 +324,7 @@ AspirateInPlaceParams, CommentParams, ConfigureForVolumeParams, + ConfigureNozzleLayoutParams, CustomParams, DispenseParams, DispenseInPlaceParams, @@ -322,6 +342,7 @@ MoveRelativeParams, MoveToCoordinatesParams, MoveToWellParams, + MoveToAddressableAreaParams, PrepareToAspirateParams, WaitForResumeParams, WaitForDurationParams, @@ -363,6 +384,7 @@ AspirateInPlaceCommandType, CommentCommandType, ConfigureForVolumeCommandType, + ConfigureNozzleLayoutCommandType, CustomCommandType, DispenseCommandType, DispenseInPlaceCommandType, @@ -380,6 +402,7 @@ MoveRelativeCommandType, MoveToCoordinatesCommandType, MoveToWellCommandType, + MoveToAddressableAreaCommandType, PrepareToAspirateCommandType, WaitForResumeCommandType, WaitForDurationCommandType, @@ -420,6 +443,7 @@ AspirateInPlaceCreate, CommentCreate, ConfigureForVolumeCreate, + ConfigureNozzleLayoutCreate, CustomCreate, DispenseCreate, DispenseInPlaceCreate, @@ -437,6 +461,7 @@ MoveRelativeCreate, MoveToCoordinatesCreate, MoveToWellCreate, + MoveToAddressableAreaCreate, PrepareToAspirateCreate, WaitForResumeCreate, WaitForDurationCreate, @@ -477,6 +502,7 @@ AspirateInPlaceResult, CommentResult, ConfigureForVolumeResult, + ConfigureNozzleLayoutResult, CustomResult, DispenseResult, DispenseInPlaceResult, @@ -494,6 +520,7 @@ MoveRelativeResult, MoveToCoordinatesResult, MoveToWellResult, + MoveToAddressableAreaResult, PrepareToAspirateResult, WaitForResumeResult, WaitForDurationResult, @@ -530,5 +557,8 @@ ] CommandPrivateResult = Union[ - None, LoadPipettePrivateResult, ConfigureForVolumePrivateResult + None, + LoadPipettePrivateResult, + ConfigureForVolumePrivateResult, + ConfigureNozzleLayoutPrivateResult, ] diff --git a/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py b/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py new file mode 100644 index 00000000000..2ad5f38a9a5 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/configure_nozzle_layout.py @@ -0,0 +1,111 @@ +"""Configure nozzle layout command request, result, and implementation models.""" +from __future__ import annotations +from pydantic import BaseModel +from typing import TYPE_CHECKING, Optional, Type, Tuple, Union +from typing_extensions import Literal + +from .pipetting_common import ( + PipetteIdMixin, +) +from .command import ( + AbstractCommandWithPrivateResultImpl, + BaseCommand, + BaseCommandCreate, +) +from .configuring_common import ( + PipetteNozzleLayoutResultMixin, +) +from ..types import ( + EmptyNozzleLayoutConfiguration, + SingleNozzleLayoutConfiguration, + RowNozzleLayoutConfiguration, + ColumnNozzleLayoutConfiguration, + QuadrantNozzleLayoutConfiguration, +) + +if TYPE_CHECKING: + from ..execution import EquipmentHandler, TipHandler + + +ConfigureNozzleLayoutCommandType = Literal["configureNozzleLayout"] + + +class ConfigureNozzleLayoutParams(PipetteIdMixin): + """Parameters required to configure the nozzle layout for a specific pipette.""" + + configuration_params: Union[ + EmptyNozzleLayoutConfiguration, + SingleNozzleLayoutConfiguration, + RowNozzleLayoutConfiguration, + ColumnNozzleLayoutConfiguration, + QuadrantNozzleLayoutConfiguration, + ] + + +class ConfigureNozzleLayoutPrivateResult(PipetteNozzleLayoutResultMixin): + """Result sent to the store but not serialized.""" + + pass + + +class ConfigureNozzleLayoutResult(BaseModel): + """Result data from execution of an configureNozzleLayout command.""" + + pass + + +class ConfigureNozzleLayoutImplementation( + AbstractCommandWithPrivateResultImpl[ + ConfigureNozzleLayoutParams, + ConfigureNozzleLayoutResult, + ConfigureNozzleLayoutPrivateResult, + ] +): + """Configure nozzle layout command implementation.""" + + def __init__( + self, equipment: EquipmentHandler, tip_handler: TipHandler, **kwargs: object + ) -> None: + self._equipment = equipment + self._tip_handler = tip_handler + + async def execute( + self, params: ConfigureNozzleLayoutParams + ) -> Tuple[ConfigureNozzleLayoutResult, ConfigureNozzleLayoutPrivateResult]: + """Check that requested pipette can support the requested nozzle layout.""" + nozzle_params = await self._tip_handler.available_for_nozzle_layout( + pipette_id=params.pipetteId, **params.configuration_params.dict() + ) + + nozzle_map = await self._equipment.configure_nozzle_layout( + pipette_id=params.pipetteId, + **nozzle_params, + ) + + return ConfigureNozzleLayoutResult(), ConfigureNozzleLayoutPrivateResult( + pipette_id=params.pipetteId, + nozzle_map=nozzle_map, + ) + + +class ConfigureNozzleLayout( + BaseCommand[ConfigureNozzleLayoutParams, ConfigureNozzleLayoutResult] +): + """Configure nozzle layout command model.""" + + commandType: ConfigureNozzleLayoutCommandType = "configureNozzleLayout" + params: ConfigureNozzleLayoutParams + result: Optional[ConfigureNozzleLayoutResult] + + _ImplementationCls: Type[ + ConfigureNozzleLayoutImplementation + ] = ConfigureNozzleLayoutImplementation + + +class ConfigureNozzleLayoutCreate(BaseCommandCreate[ConfigureNozzleLayoutParams]): + """Configure nozzle layout creation request model.""" + + commandType: ConfigureNozzleLayoutCommandType = "configureNozzleLayout" + params: ConfigureNozzleLayoutParams + + _CommandCls: Type[ConfigureNozzleLayout] = ConfigureNozzleLayout diff --git a/api/src/opentrons/protocol_engine/commands/configuring_common.py b/api/src/opentrons/protocol_engine/commands/configuring_common.py index a4ff8917310..ec5917d9931 100644 --- a/api/src/opentrons/protocol_engine/commands/configuring_common.py +++ b/api/src/opentrons/protocol_engine/commands/configuring_common.py @@ -1,6 +1,11 @@ """Common configuration command base models.""" +from pydantic import BaseModel, Field +from typing import Optional from dataclasses import dataclass +from opentrons.hardware_control.nozzle_manager import ( + NozzleMap, +) from ..resources import pipette_data_provider @@ -11,3 +16,13 @@ class PipetteConfigUpdateResultMixin: pipette_id: str serial_number: str config: pipette_data_provider.LoadedStaticPipetteData + + +class PipetteNozzleLayoutResultMixin(BaseModel): + """A nozzle layout result for updating the pipette state.""" + + pipette_id: str + nozzle_map: Optional[NozzleMap] = Field( + default=None, + description="A dataclass object holding information about the current nozzle configuration.", + ) diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip.py b/api/src/opentrons/protocol_engine/commands/drop_tip.py index 90b0c04484b..923c384e630 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip.py @@ -82,8 +82,14 @@ async def execute(self, params: DropTipParams) -> DropTipResult: else: well_location = params.wellLocation + is_partially_configured = self._state_view.pipettes.get_is_partially_configured( + pipette_id=pipette_id + ) tip_drop_location = self._state_view.geometry.get_checked_tip_drop_location( - pipette_id=pipette_id, labware_id=labware_id, well_location=well_location + pipette_id=pipette_id, + labware_id=labware_id, + well_location=well_location, + partially_configured=is_partially_configured, ) position = await self._movement_handler.move_to_well( diff --git a/api/src/opentrons/protocol_engine/commands/load_labware.py b/api/src/opentrons/protocol_engine/commands/load_labware.py index e7cee401acd..81323567d29 100644 --- a/api/src/opentrons/protocol_engine/commands/load_labware.py +++ b/api/src/opentrons/protocol_engine/commands/load_labware.py @@ -7,8 +7,13 @@ from opentrons_shared_data.labware.labware_definition import LabwareDefinition from ..errors import LabwareIsNotAllowedInLocationError -from ..resources import labware_validation -from ..types import LabwareLocation, OnLabwareLocation, DeckSlotLocation +from ..resources import labware_validation, fixture_validation +from ..types import ( + LabwareLocation, + OnLabwareLocation, + DeckSlotLocation, + AddressableAreaLocation, +) from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate @@ -105,6 +110,12 @@ async def execute(self, params: LoadLabwareParams) -> LoadLabwareResult: f"{params.loadName} is not allowed in slot {params.location.slotName}" ) + if isinstance(params.location, AddressableAreaLocation): + if not fixture_validation.is_deck_slot(params.location.addressableAreaName): + raise LabwareIsNotAllowedInLocationError( + f"Cannot load {params.loadName} onto addressable area {params.location.addressableAreaName}" + ) + verified_location = self._state_view.geometry.ensure_location_not_occupied( params.location ) diff --git a/api/src/opentrons/protocol_engine/commands/move_labware.py b/api/src/opentrons/protocol_engine/commands/move_labware.py index 682f2a58a22..49bb9920153 100644 --- a/api/src/opentrons/protocol_engine/commands/move_labware.py +++ b/api/src/opentrons/protocol_engine/commands/move_labware.py @@ -8,12 +8,13 @@ from ..types import ( LabwareLocation, OnLabwareLocation, + AddressableAreaLocation, LabwareMovementStrategy, LabwareOffsetVector, LabwareMovementOffsetData, ) from ..errors import LabwareMovementNotAllowedError, NotSupportedOnRobotType -from ..resources import labware_validation +from ..resources import labware_validation, fixture_validation from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate if TYPE_CHECKING: @@ -83,7 +84,9 @@ def __init__( self._labware_movement = labware_movement self._run_control = run_control - async def execute(self, params: MoveLabwareParams) -> MoveLabwareResult: + async def execute( # noqa: C901 + self, params: MoveLabwareParams + ) -> MoveLabwareResult: """Move a loaded labware to a new location.""" # Allow propagation of LabwareNotLoadedError. current_labware = self._state_view.labware.get(labware_id=params.labwareId) @@ -97,6 +100,15 @@ async def execute(self, params: MoveLabwareParams) -> MoveLabwareResult: f"Cannot move fixed trash labware '{current_labware_definition.parameters.loadName}'." ) + if isinstance(params.newLocation, AddressableAreaLocation): + area_name = params.newLocation.addressableAreaName + if not fixture_validation.is_gripper_waste_chute( + area_name + ) and not fixture_validation.is_deck_slot(area_name): + raise LabwareMovementNotAllowedError( + f"Cannot move {current_labware.loadName} to addressable area {area_name}" + ) + available_new_location = self._state_view.geometry.ensure_location_not_occupied( location=params.newLocation ) diff --git a/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py new file mode 100644 index 00000000000..822b99e7921 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/move_to_addressable_area.py @@ -0,0 +1,73 @@ +"""Move to well command request, result, and implementation models.""" +from __future__ import annotations +from pydantic import Field +from typing import TYPE_CHECKING, Optional, Type +from typing_extensions import Literal + +from .pipetting_common import ( + PipetteIdMixin, + MovementMixin, + DestinationPositionResult, +) +from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate + +if TYPE_CHECKING: + from ..execution import MovementHandler + +MoveToAddressableAreaCommandType = Literal["moveToAddressableArea"] + + +class MoveToAddressableAreaParams(PipetteIdMixin, MovementMixin): + """Payload required to move a pipette to a specific addressable area.""" + + addressableAreaName: str = Field( + description=( + "The name of the addressable area that you want to use." + " Valid values are the `id`s of `addressableArea`s in the" + " [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck)." + ) + ) + + +class MoveToAddressableAreaResult(DestinationPositionResult): + """Result data from the execution of a MoveToAddressableArea command.""" + + pass + + +class MoveToAddressableAreaImplementation( + AbstractCommandImpl[MoveToAddressableAreaParams, MoveToAddressableAreaResult] +): + """Move to addressable area command implementation.""" + + def __init__(self, movement: MovementHandler, **kwargs: object) -> None: + self._movement = movement + + async def execute( + self, params: MoveToAddressableAreaParams + ) -> MoveToAddressableAreaResult: + """Move the requested pipette to the requested addressable area.""" + return MoveToAddressableAreaResult() + + +class MoveToAddressableArea( + BaseCommand[MoveToAddressableAreaParams, MoveToAddressableAreaResult] +): + """Move to addressable area command model.""" + + commandType: MoveToAddressableAreaCommandType = "moveToAddressableArea" + params: MoveToAddressableAreaParams + result: Optional[MoveToAddressableAreaResult] + + _ImplementationCls: Type[ + MoveToAddressableAreaImplementation + ] = MoveToAddressableAreaImplementation + + +class MoveToAddressableAreaCreate(BaseCommandCreate[MoveToAddressableAreaParams]): + """Move to addressable area command creation request model.""" + + commandType: MoveToAddressableAreaCommandType = "moveToAddressableArea" + params: MoveToAddressableAreaParams + + _CommandCls: Type[MoveToAddressableArea] = MoveToAddressableArea diff --git a/api/src/opentrons/protocol_engine/create_protocol_engine.py b/api/src/opentrons/protocol_engine/create_protocol_engine.py index adb4657d2af..cb94dfef918 100644 --- a/api/src/opentrons/protocol_engine/create_protocol_engine.py +++ b/api/src/opentrons/protocol_engine/create_protocol_engine.py @@ -10,20 +10,24 @@ from .protocol_engine import ProtocolEngine from .resources import DeckDataProvider, ModuleDataProvider from .state import Config, StateStore -from .types import PostRunHardwareState +from .types import PostRunHardwareState, DeckConfigurationType # TODO(mm, 2023-06-16): Arguably, this not being a context manager makes us prone to forgetting to # clean it up properly, especially in tests. See e.g. https://opentrons.atlassian.net/browse/RSS-222 async def create_protocol_engine( - hardware_api: HardwareControlAPI, config: Config, load_fixed_trash: bool = False + hardware_api: HardwareControlAPI, + config: Config, + load_fixed_trash: bool = False, + deck_configuration: typing.Optional[DeckConfigurationType] = None, ) -> ProtocolEngine: """Create a ProtocolEngine instance. Arguments: hardware_api: Hardware control API to pass down to dependencies. config: ProtocolEngine configuration. - load_fixed_trash: Automatically load fixed trash labware in engine + load_fixed_trash: Automatically load fixed trash labware in engine. + deck_configuration: The initial deck configuration the engine will be instantiated with. """ deck_data = DeckDataProvider(config.deck_type) deck_definition = await deck_data.get_deck_definition() @@ -40,6 +44,7 @@ async def create_protocol_engine( deck_fixed_labware=deck_fixed_labware, is_door_open=hardware_api.door_state is DoorState.OPEN, module_calibration_offsets=module_calibration_offsets, + deck_configuration=deck_configuration, ) return ProtocolEngine(state_store=state_store, hardware_api=hardware_api) diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index 642d4ff6cd8..0e0a4148fec 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -27,7 +27,12 @@ ModuleNotOnDeckError, ModuleNotConnectedError, SlotDoesNotExistError, + CutoutDoesNotExistError, FixtureDoesNotExistError, + AddressableAreaDoesNotExistError, + FixtureDoesNotProvideAreasError, + AreaNotInDeckConfigurationError, + IncompatibleAddressableAreaError, FailedToPlanMoveError, MustHomeError, RunStoppedError, @@ -88,7 +93,12 @@ "ModuleNotOnDeckError", "ModuleNotConnectedError", "SlotDoesNotExistError", + "CutoutDoesNotExistError", "FixtureDoesNotExistError", + "AddressableAreaDoesNotExistError", + "FixtureDoesNotProvideAreasError", + "AreaNotInDeckConfigurationError", + "IncompatibleAddressableAreaError", "FailedToPlanMoveError", "MustHomeError", "RunStoppedError", diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index 7f4304f8097..2527077f58d 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -373,8 +373,21 @@ def __init__( super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) +class CutoutDoesNotExistError(ProtocolEngineError): + """Raised when referencing a cutout that does not exist.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a CutoutDoesNotExistError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + class FixtureDoesNotExistError(ProtocolEngineError): - """Raised when referencing an addressable area (aka fixture) that does not exist.""" + """Raised when referencing a cutout fixture that does not exist.""" def __init__( self, @@ -386,6 +399,58 @@ def __init__( super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) +class AddressableAreaDoesNotExistError(ProtocolEngineError): + """Raised when referencing an addressable area that does not exist.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a AddressableAreaDoesNotExistError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + +class FixtureDoesNotProvideAreasError(ProtocolEngineError): + """Raised when a cutout fixture does not provide any addressable areas for a requested cutout.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a FixtureDoesNotProvideAreasError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + +class AreaNotInDeckConfigurationError(ProtocolEngineError): + """Raised when an addressable area is referenced that is not provided by a deck configuration.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a AreaNotInDeckConfigurationError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + +class IncompatibleAddressableAreaError(ProtocolEngineError): + """Raised when two non-compatible addressable areas are referenced during analysis.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a IncompatibleAddressableAreaError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + # TODO(mc, 2020-11-06): flesh out with structured data to replicate # existing LabwareHeightError class FailedToPlanMoveError(ProtocolEngineError): diff --git a/api/src/opentrons/protocol_engine/execution/equipment.py b/api/src/opentrons/protocol_engine/execution/equipment.py index 4402613d975..c1ac272a64d 100644 --- a/api/src/opentrons/protocol_engine/execution/equipment.py +++ b/api/src/opentrons/protocol_engine/execution/equipment.py @@ -15,6 +15,7 @@ TempDeck, Thermocycler, ) +from opentrons.hardware_control.nozzle_manager import NozzleMap from opentrons.protocol_engine.state.module_substates import ( MagneticModuleId, HeaterShakerModuleId, @@ -383,6 +384,55 @@ async def configure_for_volume( static_config=static_pipette_config, ) + async def configure_nozzle_layout( + self, + pipette_id: str, + primary_nozzle: Optional[str] = None, + front_right_nozzle: Optional[str] = None, + back_left_nozzle: Optional[str] = None, + ) -> Optional[NozzleMap]: + """Ensure the requested nozzle layout is compatible with the current pipette. + + Args: + pipette_id: The identifier for the pipette. + primary_nozzle: The nozzle which will be used as the + front_right_nozzle + back_left_nozzle + + Returns: + A NozzleMap object or None. + """ + use_virtual_pipettes = self._state_store.config.use_virtual_pipettes + + if not use_virtual_pipettes: + mount = self._state_store.pipettes.get_mount(pipette_id).to_hw_mount() + + await self._hardware_api.update_nozzle_configuration_for_mount( + mount, + back_left_nozzle if back_left_nozzle else primary_nozzle, + front_right_nozzle if front_right_nozzle else primary_nozzle, + primary_nozzle if back_left_nozzle else None, + ) + pipette_dict = self._hardware_api.get_attached_instrument(mount) + nozzle_map = pipette_dict["current_nozzle_map"] + + else: + model = self._state_store.pipettes.get_model_name(pipette_id) + self._virtual_pipette_data_provider.configure_virtual_pipette_nozzle_layout( + pipette_id, + model, + back_left_nozzle if back_left_nozzle else primary_nozzle, + front_right_nozzle if front_right_nozzle else primary_nozzle, + primary_nozzle if back_left_nozzle else None, + ) + nozzle_map = ( + self._virtual_pipette_data_provider.get_nozzle_layout_for_pipette( + pipette_id + ) + ) + + return nozzle_map + @overload def get_module_hardware_api( self, diff --git a/api/src/opentrons/protocol_engine/execution/tip_handler.py b/api/src/opentrons/protocol_engine/execution/tip_handler.py index b007545edd2..b04750a343a 100644 --- a/api/src/opentrons/protocol_engine/execution/tip_handler.py +++ b/api/src/opentrons/protocol_engine/execution/tip_handler.py @@ -1,17 +1,43 @@ """Tip pickup and drop procedures.""" -from typing import Optional +from typing import Optional, Dict from typing_extensions import Protocol as TypingProtocol from opentrons.hardware_control import HardwareControlAPI +from opentrons_shared_data.errors.exceptions import ( + CommandPreconditionViolated, + CommandParameterLimitViolated, +) from ..resources import LabwareDataProvider from ..state import StateView from ..types import TipGeometry +PRIMARY_NOZZLE_TO_ENDING_NOZZLE_MAP = { + "A1": {"COLUMN": "H1", "ROW": "A12"}, + "H1": {"COLUMN": "A1", "ROW": "H12"}, + "A12": {"COLUMN": "H12", "ROW": "A1"}, + "H12": {"COLUMN": "A12", "ROW": "H1"}, +} + + class TipHandler(TypingProtocol): """Pick up and drop tips.""" + async def available_for_nozzle_layout( + self, + pipette_id: str, + style: str, + primary_nozzle: Optional[str] = None, + front_right_nozzle: Optional[str] = None, + ) -> Dict[str, str]: + """Check nozzle layout is compatible with the pipette. + + Returns: + A dict of nozzles used to configure the pipette. + """ + ... + async def pick_up_tip( self, pipette_id: str, @@ -37,6 +63,47 @@ async def add_tip(self, pipette_id: str, tip: TipGeometry) -> None: """Tell the Hardware API that a tip is attached.""" +async def _available_for_nozzle_layout( + channels: int, + style: str, + primary_nozzle: Optional[str], + front_right_nozzle: Optional[str], +) -> Dict[str, str]: + """Check nozzle layout is compatible with the pipette. + + Returns: + A dict of nozzles used to configure the pipette. + """ + if channels == 1: + raise CommandPreconditionViolated( + message=f"Cannot configure nozzle layout with a {channels} channel pipette." + ) + if style == "EMPTY": + return {} + if style == "ROW" and channels == 8: + raise CommandParameterLimitViolated( + command_name="configure_nozzle_layout", + parameter_name="RowNozzleLayout", + limit_statement="RowNozzleLayout is incompatible with {channels} channel pipettes.", + actual_value=str(primary_nozzle), + ) + if not primary_nozzle: + return {"primary_nozzle": "A1"} + if style == "SINGLE": + return {"primary_nozzle": primary_nozzle} + if not front_right_nozzle: + return { + "primary_nozzle": primary_nozzle, + "front_right_nozzle": PRIMARY_NOZZLE_TO_ENDING_NOZZLE_MAP[primary_nozzle][ + style + ], + } + return { + "primary_nozzle": primary_nozzle, + "front_right_nozzle": front_right_nozzle, + } + + class HardwareTipHandler(TipHandler): """Pick up and drop tips, using the Hardware API.""" @@ -46,9 +113,26 @@ def __init__( hardware_api: HardwareControlAPI, labware_data_provider: Optional[LabwareDataProvider] = None, ) -> None: - self._state_view = state_view self._hardware_api = hardware_api self._labware_data_provider = labware_data_provider or LabwareDataProvider() + self._state_view = state_view + + async def available_for_nozzle_layout( + self, + pipette_id: str, + style: str, + primary_nozzle: Optional[str] = None, + front_right_nozzle: Optional[str] = None, + ) -> Dict[str, str]: + """Returns configuration for nozzle layout to pass to configure_nozzle_layout.""" + if self._state_view.pipettes.get_attached_tip(pipette_id): + raise CommandPreconditionViolated( + message=f"Cannot configure nozzle layout of {str(self)} while it has tips attached." + ) + channels = self._state_view.pipettes.get_channels(pipette_id) + return await _available_for_nozzle_layout( + channels, style, primary_nozzle, front_right_nozzle + ) async def pick_up_tip( self, @@ -152,6 +236,23 @@ async def pick_up_tip( return nominal_tip_geometry + async def available_for_nozzle_layout( + self, + pipette_id: str, + style: str, + primary_nozzle: Optional[str] = None, + front_right_nozzle: Optional[str] = None, + ) -> Dict[str, str]: + """Returns configuration for nozzle layout to pass to configure_nozzle_layout.""" + if self._state_view.pipettes.get_attached_tip(pipette_id): + raise CommandPreconditionViolated( + message=f"Cannot configure nozzle layout of {str(self)} while it has tips attached." + ) + channels = self._state_view.pipettes.get_channels(pipette_id) + return await _available_for_nozzle_layout( + channels, style, primary_nozzle, front_right_nozzle + ) + async def drop_tip( self, pipette_id: str, diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index 857d787dcd4..a04f24d347a 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -24,6 +24,7 @@ Liquid, HexColor, PostRunHardwareState, + DeckConfigurationType, ) from .execution import ( QueueWorker, @@ -133,13 +134,13 @@ def add_plugin(self, plugin: AbstractPlugin) -> None: """Add a plugin to the engine to customize behavior.""" self._plugin_starter.start(plugin) - def play(self) -> None: + def play(self, deck_configuration: Optional[DeckConfigurationType] = None) -> None: """Start or resume executing commands in the queue.""" requested_at = self._model_utils.get_timestamp() # TODO(mc, 2021-08-05): if starting, ensure plungers motors are # homed if necessary action = self._state_store.commands.validate_action_allowed( - PlayAction(requested_at=requested_at) + PlayAction(requested_at=requested_at, deck_configuration=deck_configuration) ) self._action_dispatcher.dispatch(action) diff --git a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py index cc24a572a70..144f59f3e20 100644 --- a/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py +++ b/api/src/opentrons/protocol_engine/resources/deck_configuration_provider.py @@ -1,72 +1,139 @@ """Deck configuration resource provider.""" -from dataclasses import dataclass -from typing import List, Set, Dict +from typing import List, Set, Tuple, Optional -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, AddressableArea +from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, CutoutFixture +from opentrons.types import Point -from ..errors import FixtureDoesNotExistError +from ..types import ( + AddressableArea, + PotentialCutoutFixture, + DeckPoint, + Dimensions, + AddressableOffsetVector, +) +from ..errors import ( + CutoutDoesNotExistError, + FixtureDoesNotExistError, + AddressableAreaDoesNotExistError, + FixtureDoesNotProvideAreasError, +) -@dataclass(frozen=True) -class DeckCutoutFixture: - """Basic cutout fixture data class.""" +def get_cutout_position(cutout_id: str, deck_definition: DeckDefinitionV4) -> DeckPoint: + """Get the base position of a cutout on the deck.""" + for cutout in deck_definition["locations"]["cutouts"]: + if cutout_id == cutout["id"]: + position = cutout["position"] + return DeckPoint(x=position[0], y=position[1], z=position[2]) + else: + raise CutoutDoesNotExistError(f"Could not find cutout with name {cutout_id}") - name: str - # TODO(jbl 10-30-2023) this is in reference to the cutout ID that is supplied in mayMountTo in the definition. - # We might want to make this not a string. - cutout_slot_location: str +def get_cutout_fixture( + cutout_fixture_id: str, deck_definition: DeckDefinitionV4 +) -> CutoutFixture: + """Gets cutout fixture from deck that matches the cutout fixture ID provided.""" + for cutout_fixture in deck_definition["cutoutFixtures"]: + if cutout_fixture["id"] == cutout_fixture_id: + return cutout_fixture + raise FixtureDoesNotExistError( + f"Could not find cutout fixture with name {cutout_fixture_id}" + ) -class DeckConfigurationProvider: - """Provider class to ingest deck configuration data and retrieve relevant deck definition data.""" - _configuration: Dict[str, DeckCutoutFixture] +def get_provided_addressable_area_names( + cutout_fixture_id: str, cutout_id: str, deck_definition: DeckDefinitionV4 +) -> List[str]: + """Gets a list of the addressable areas provided by the cutout fixture on the cutout.""" + cutout_fixture = get_cutout_fixture(cutout_fixture_id, deck_definition) + try: + return cutout_fixture["providesAddressableAreas"][cutout_id] + except KeyError as exception: + raise FixtureDoesNotProvideAreasError( + f"Cutout fixture {cutout_fixture['id']} does not provide addressable areas for {cutout_id}" + ) from exception - def __init__( - self, - deck_definition: DeckDefinitionV4, - deck_configuration: List[DeckCutoutFixture], - ) -> None: - """Initialize a DeckDataProvider.""" - self._deck_definition = deck_definition - self._configuration = { - cutout_fixture.cutout_slot_location: cutout_fixture - for cutout_fixture in deck_configuration - } - def get_addressable_areas_for_cutout_fixture( - self, cutout_fixture_id: str, cutout_id: str - ) -> Set[str]: - """Get the allowable addressable areas for a cutout fixture loaded on a specific cutout slot.""" - for cutout_fixture in self._deck_definition["cutoutFixtures"]: - if cutout_fixture_id == cutout_fixture["id"]: - return set( - cutout_fixture["providesAddressableAreas"].get(cutout_id, []) +def get_potential_cutout_fixtures( + addressable_area_name: str, deck_definition: DeckDefinitionV4 +) -> Tuple[str, Set[PotentialCutoutFixture]]: + """Given an addressable area name, gets the cutout ID associated with it and a set of potential fixtures.""" + potential_fixtures = [] + for cutout_fixture in deck_definition["cutoutFixtures"]: + for cutout_id, provided_areas in cutout_fixture[ + "providesAddressableAreas" + ].items(): + if addressable_area_name in provided_areas: + potential_fixtures.append( + PotentialCutoutFixture( + cutout_id=cutout_id, + cutout_fixture_id=cutout_fixture["id"], + ) ) + # This following logic is making the assumption that every addressable area can only go on one cutout, though + # it may have multiple cutout fixtures that supply it on that cutout. If this assumption changes, some of the + # following logic will have to be readjusted + assert ( + potential_fixtures + ), f"No potential fixtures for addressable area {addressable_area_name}" + cutout_id = potential_fixtures[0].cutout_id + assert all(cutout_id == fixture.cutout_id for fixture in potential_fixtures) + return cutout_id, set(potential_fixtures) - raise FixtureDoesNotExistError( - f'Could not resolve "{cutout_fixture_id}" to a fixture.' - ) - def get_configured_addressable_areas(self) -> Set[str]: - """Get a list of all addressable areas the robot is configured for.""" - configured_addressable_areas = set() - for cutout_id, cutout_fixture in self._configuration.items(): - addressable_areas = self.get_addressable_areas_for_cutout_fixture( - cutout_fixture.name, cutout_id +def get_addressable_area_from_name( + addressable_area_name: str, + cutout_position: DeckPoint, + deck_definition: DeckDefinitionV4, +) -> AddressableArea: + """Given a name and a cutout position, get an addressable area on the deck.""" + for addressable_area in deck_definition["locations"]["addressableAreas"]: + if addressable_area["id"] == addressable_area_name: + area_offset = addressable_area["offsetFromCutoutFixture"] + position = AddressableOffsetVector( + x=area_offset[0] + cutout_position.x, + y=area_offset[1] + cutout_position.y, + z=area_offset[2] + cutout_position.z, ) - configured_addressable_areas.update(addressable_areas) - return configured_addressable_areas + bounding_box = Dimensions( + x=addressable_area["boundingBox"]["xDimension"], + y=addressable_area["boundingBox"]["yDimension"], + z=addressable_area["boundingBox"]["zDimension"], + ) + drop_tips_deck_offset = addressable_area.get("dropTipsOffset") + drop_tip_location: Optional[Point] + if drop_tips_deck_offset: + drop_tip_location = Point( + x=drop_tips_deck_offset[0] + cutout_position.x, + y=drop_tips_deck_offset[1] + cutout_position.y, + z=drop_tips_deck_offset[2] + cutout_position.z, + ) + else: + drop_tip_location = None - def get_addressable_area_definition( - self, addressable_area_name: str - ) -> AddressableArea: - """Get the addressable area definition from the relevant deck definition.""" - for addressable_area in self._deck_definition["locations"]["addressableAreas"]: - if addressable_area_name == addressable_area["id"]: - return addressable_area + drop_labware_deck_offset = addressable_area.get("dropLabwareOffset") + drop_labware_location: Optional[Point] + if drop_labware_deck_offset: + drop_labware_location = Point( + x=drop_labware_deck_offset[0] + cutout_position.x, + y=drop_labware_deck_offset[1] + cutout_position.y, + z=drop_labware_deck_offset[2] + cutout_position.z, + ) + else: + drop_labware_location = None - raise FixtureDoesNotExistError( - f'Could not resolve "{addressable_area_name}" to a fixture.' - ) + return AddressableArea( + area_name=addressable_area["id"], + display_name=addressable_area["displayName"], + bounding_box=bounding_box, + position=position, + compatible_module_types=addressable_area.get( + "compatibleModuleTypes", [] + ), + drop_tip_location=drop_tip_location, + drop_labware_location=drop_labware_location, + ) + raise AddressableAreaDoesNotExistError( + f"Could not find addressable area with name {addressable_area_name}" + ) diff --git a/api/src/opentrons/protocol_engine/resources/fixture_validation.py b/api/src/opentrons/protocol_engine/resources/fixture_validation.py index 3eed2f90b22..bee59415d11 100644 --- a/api/src/opentrons/protocol_engine/resources/fixture_validation.py +++ b/api/src/opentrons/protocol_engine/resources/fixture_validation.py @@ -1,69 +1,43 @@ -"""Validation file for fixtures and addressable area reference checking functions.""" +"""Validation file for addressable area reference checking functions.""" -from typing import List +from opentrons.types import DeckSlotName -from opentrons_shared_data.deck.deck_definitions import Locations, CutoutFixture -from opentrons.hardware_control.modules.types import ModuleModel, ModuleType +def is_waste_chute(addressable_area_name: str) -> bool: + """Check if an addressable area is a Waste Chute.""" + return addressable_area_name in { + "1and8ChannelWasteChute", + "96ChannelWasteChute", + "gripperWasteChute", + } -def validate_fixture_id(fixtureList: List[CutoutFixture], load_name: str) -> bool: - """Check that the loaded fixture has an existing definition.""" - for fixture in fixtureList: - if fixture.id == load_name: - return True - return False +def is_gripper_waste_chute(addressable_area_name: str) -> bool: + """Check if an addressable area is a gripper-movement-compatible Waste Chute.""" + return addressable_area_name == "gripperWasteChute" -def validate_fixture_location_is_allowed(fixture: CutoutFixture, location: str) -> bool: - """Validate that the fixture is allowed to load into the provided location according to the deck definitions.""" - return location in fixture.mayMountTo +def is_drop_tip_waste_chute(addressable_area_name: str) -> bool: + """Check if an addressable area is a Waste Chute compatible for dropping tips.""" + return addressable_area_name in {"1and8ChannelWasteChute", "96ChannelWasteChute"} -def validate_is_wastechute(load_name: str) -> bool: - """Check if a fixture is a Waste Chute.""" - return ( - load_name == "wasteChuteRightAdapterCovered" - or load_name == "wasteChuteRightAdapterNoCover" - or load_name == "stagingAreaSlotWithWasteChuteRightAdapterCovered" - or load_name == "stagingAreaSlotWithWasteChuteRightAdapterNoCover" - ) +def is_trash(addressable_area_name: str) -> bool: + """Check if an addressable area is a trash bin.""" + return addressable_area_name in {"movableTrash", "fixedTrash", "shortFixedTrash"} -def validate_module_is_compatible_with_fixture( - locations: Locations, fixture: CutoutFixture, module: ModuleModel -) -> bool: - """Validate that the fixture allows the loading of a specified module.""" - module_name = ModuleType.from_model(module).name - for key in fixture.providesAddressableAreas.keys(): - for area in fixture.providesAddressableAreas[key]: - for l_area in locations.addressableAreas: - if l_area.id == area: - if l_area.compatibleModuleTypes is None: - return False - elif module_name in l_area.compatibleModuleTypes: - return True - return False +def is_staging_slot(addressable_area_name: str) -> bool: + """Check if an addressable area is a staging area slot.""" + return addressable_area_name in {"A4", "B4", "C4", "D4"} -def validate_fixture_allows_drop_tip( - locations: Locations, fixture: CutoutFixture -) -> bool: - """Validate that the fixture allows tips to be dropped in it's addressable areas.""" - for key in fixture.providesAddressableAreas.keys(): - for area in fixture.providesAddressableAreas[key]: - for l_area in locations.addressableAreas: - if l_area.id == area and l_area.ableToDropTips: - return True - return False - -def validate_fixture_allows_drop_labware( - locations: Locations, fixture: CutoutFixture -) -> bool: - """Validate that the fixture allows labware to be dropped in it's addressable areas.""" - for key in fixture.providesAddressableAreas.keys(): - for area in fixture.providesAddressableAreas[key]: - for l_area in locations.addressableAreas: - if l_area.id == area and l_area.ableToDropLabware: - return True - return False +def is_deck_slot(addressable_area_name: str) -> bool: + """Check if an addressable area is a deck slot (including staging area slots).""" + if is_staging_slot(addressable_area_name): + return True + try: + DeckSlotName.from_primitive(addressable_area_name) + except ValueError: + return False + return True diff --git a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py index 086c670fb5d..b3bf334933f 100644 --- a/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py +++ b/api/src/opentrons/protocol_engine/resources/pipette_data_provider.py @@ -1,6 +1,6 @@ """Pipette config data providers.""" from dataclasses import dataclass -from typing import Dict +from typing import Dict, Optional from opentrons_shared_data.pipette.dev_types import PipetteName, PipetteModel from opentrons_shared_data.pipette import ( @@ -12,6 +12,10 @@ from opentrons.hardware_control.dev_types import PipetteDict +from opentrons.hardware_control.nozzle_manager import ( + NozzleConfigurationManager, + NozzleMap, +) from ..types import FlowRates @@ -40,6 +44,36 @@ class VirtualPipetteDataProvider: def __init__(self) -> None: """Build a VirtualPipetteDataProvider.""" self._liquid_class_by_id: Dict[str, pip_types.LiquidClasses] = {} + self._nozzle_manager_layout_by_id: Dict[str, NozzleConfigurationManager] = {} + + def configure_virtual_pipette_nozzle_layout( + self, + pipette_id: str, + pipette_model_string: str, + back_left_nozzle: Optional[str] = None, + front_right_nozzle: Optional[str] = None, + starting_nozzle: Optional[str] = None, + ) -> None: + """Emulate update_nozzle_configuration_for_mount.""" + if pipette_id not in self._nozzle_manager_layout_by_id: + config = self._get_virtual_pipette_full_config_by_model_string( + pipette_model_string + ) + new_nozzle_manager = NozzleConfigurationManager.build_from_config(config) + if back_left_nozzle and front_right_nozzle: + new_nozzle_manager.update_nozzle_configuration( + back_left_nozzle, front_right_nozzle, starting_nozzle + ) + self._nozzle_manager_layout_by_id[pipette_id] = new_nozzle_manager + elif back_left_nozzle and front_right_nozzle: + # Need to make sure that we pass all the right nozzles here. + self._nozzle_manager_layout_by_id[pipette_id].update_nozzle_configuration( + back_left_nozzle, front_right_nozzle, starting_nozzle + ) + else: + self._nozzle_manager_layout_by_id[ + pipette_id + ].reset_to_default_configuration() def configure_virtual_pipette_for_volume( self, pipette_id: str, volume: float, pipette_model_string: str @@ -61,6 +95,10 @@ def configure_virtual_pipette_for_volume( ) self._liquid_class_by_id[pipette_id] = liquid_class + def get_nozzle_layout_for_pipette(self, pipette_id: str) -> NozzleMap: + """Get the current nozzle layout stored for a virtual pipette.""" + return self._nozzle_manager_layout_by_id[pipette_id].current_configuration + def get_virtual_pipette_static_config_by_model_string( self, pipette_model_string: str, pipette_id: str ) -> LoadedStaticPipetteData: @@ -72,6 +110,19 @@ def get_virtual_pipette_static_config_by_model_string( pipette_model, pipette_id ) + def _get_virtual_pipette_full_config_by_model_string( + self, pipette_model_string: str + ) -> pipette_definition.PipetteConfigurations: + """Get the full pipette config from a model string.""" + pipette_model = pipette_load_name.convert_pipette_model( + PipetteModel(pipette_model_string) + ) + return load_pipette_data.load_definition( + pipette_model.pipette_type, + pipette_model.pipette_channels, + pipette_model.pipette_version, + ) + def _get_virtual_pipette_static_config_by_model( self, pipette_model: pipette_definition.PipetteModelVersionType, pipette_id: str ) -> LoadedStaticPipetteData: diff --git a/api/src/opentrons/protocol_engine/slot_standardization.py b/api/src/opentrons/protocol_engine/slot_standardization.py index 9b2e352393a..c4e733b3ca6 100644 --- a/api/src/opentrons/protocol_engine/slot_standardization.py +++ b/api/src/opentrons/protocol_engine/slot_standardization.py @@ -24,7 +24,7 @@ OFF_DECK_LOCATION, DeckSlotLocation, LabwareLocation, - NonStackedLocation, + AddressableAreaLocation, LabwareOffsetCreate, ModuleLocation, OnLabwareLocation, @@ -124,21 +124,14 @@ def _standardize_labware_location( if isinstance(original, DeckSlotLocation): return _standardize_deck_slot_location(original, robot_type) elif ( - isinstance(original, (ModuleLocation, OnLabwareLocation)) + isinstance( + original, (ModuleLocation, OnLabwareLocation, AddressableAreaLocation) + ) or original == OFF_DECK_LOCATION ): return original -def _standardize_adapter_location( - original: NonStackedLocation, robot_type: RobotType -) -> NonStackedLocation: - if isinstance(original, DeckSlotLocation): - return _standardize_deck_slot_location(original, robot_type) - elif isinstance(original, ModuleLocation) or original == OFF_DECK_LOCATION: - return original - - def _standardize_deck_slot_location( original: DeckSlotLocation, robot_type: RobotType ) -> DeckSlotLocation: diff --git a/api/src/opentrons/protocol_engine/state/addressable_areas.py b/api/src/opentrons/protocol_engine/state/addressable_areas.py new file mode 100644 index 00000000000..04abca05a2e --- /dev/null +++ b/api/src/opentrons/protocol_engine/state/addressable_areas.py @@ -0,0 +1,346 @@ +"""Basic addressable area data state and store.""" +from dataclasses import dataclass +from typing import Dict, Set, Union, List, Tuple + +from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3 + +from opentrons.types import Point, DeckSlotName + +from ..commands import ( + Command, + LoadLabwareResult, + LoadModuleResult, + MoveLabwareResult, + MoveToAddressableAreaResult, +) +from ..errors import ( + IncompatibleAddressableAreaError, + AreaNotInDeckConfigurationError, + SlotDoesNotExistError, +) +from ..resources import deck_configuration_provider +from ..types import ( + DeckSlotLocation, + AddressableAreaLocation, + AddressableArea, + PotentialCutoutFixture, + DeckConfigurationType, +) +from ..actions import Action, UpdateCommandAction, PlayAction +from .config import Config +from .abstract_store import HasState, HandlesActions + + +@dataclass +class AddressableAreaState: + """State of all loaded addressable area resources.""" + + loaded_addressable_areas_by_name: Dict[str, AddressableArea] + potential_cutout_fixtures_by_cutout_id: Dict[str, Set[PotentialCutoutFixture]] + deck_definition: DeckDefinitionV4 + use_simulated_deck_config: bool + + +def _get_conflicting_addressable_areas( + potential_cutout_fixtures: Set[PotentialCutoutFixture], + loaded_addressable_areas: Set[str], + deck_definition: DeckDefinitionV4, +) -> Set[str]: + loaded_areas_on_cutout = set() + for fixture in potential_cutout_fixtures: + loaded_areas_on_cutout.update( + deck_configuration_provider.get_provided_addressable_area_names( + fixture.cutout_fixture_id, + fixture.cutout_id, + deck_definition, + ) + ) + loaded_areas_on_cutout.intersection_update(loaded_addressable_areas) + return loaded_areas_on_cutout + + +# TODO make the below some sort of better type +DeckConfiguration = List[Tuple[str, str]] # cutout_id, cutout_fixture_id + + +class AddressableAreaStore(HasState[AddressableAreaState], HandlesActions): + """Addressable area state container.""" + + _state: AddressableAreaState + + def __init__( + self, + deck_configuration: DeckConfigurationType, + config: Config, + deck_definition: DeckDefinitionV4, + ) -> None: + """Initialize an addressable area store and its state.""" + if config.use_simulated_deck_config: + loaded_addressable_areas_by_name = {} + else: + loaded_addressable_areas_by_name = ( + self._get_addressable_areas_from_deck_configuration( + deck_configuration, + deck_definition, + ) + ) + self._state = AddressableAreaState( + loaded_addressable_areas_by_name=loaded_addressable_areas_by_name, + potential_cutout_fixtures_by_cutout_id={}, + deck_definition=deck_definition, + use_simulated_deck_config=config.use_simulated_deck_config, + ) + + def handle_action(self, action: Action) -> None: + """Modify state in reaction to an action.""" + if isinstance(action, UpdateCommandAction): + self._handle_command(action.command) + if isinstance(action, PlayAction): + current_state = self._state + if action.deck_configuration is not None: + self._state.loaded_addressable_areas_by_name = ( + self._get_addressable_areas_from_deck_configuration( + deck_config=action.deck_configuration, + deck_definition=current_state.deck_definition, + ) + ) + + def _handle_command(self, command: Command) -> None: + """Modify state in reaction to a command.""" + if isinstance(command.result, LoadLabwareResult): + location = command.params.location + if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)): + self._check_location_is_addressable_area(location) + + elif isinstance(command.result, MoveLabwareResult): + location = command.params.newLocation + if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)): + self._check_location_is_addressable_area(location) + + elif isinstance(command.result, LoadModuleResult): + self._check_location_is_addressable_area(command.params.location) + + elif isinstance(command.result, MoveToAddressableAreaResult): + addressable_area_name = command.params.addressableAreaName + self._check_location_is_addressable_area(addressable_area_name) + + @staticmethod + def _get_addressable_areas_from_deck_configuration( + deck_config: DeckConfiguration, deck_definition: DeckDefinitionV4 + ) -> Dict[str, AddressableArea]: + """Load all provided addressable areas with a valid deck configuration.""" + # TODO uncomment once execute is hooked up with this properly + # assert ( + # len(deck_config) == 12 + # ), f"{len(deck_config)} cutout fixture ids provided." + addressable_areas = [] + for cutout_id, cutout_fixture_id in deck_config: + provided_addressable_areas = ( + deck_configuration_provider.get_provided_addressable_area_names( + cutout_fixture_id, cutout_id, deck_definition + ) + ) + cutout_position = deck_configuration_provider.get_cutout_position( + cutout_id, deck_definition + ) + for addressable_area_name in provided_addressable_areas: + addressable_areas.append( + deck_configuration_provider.get_addressable_area_from_name( + addressable_area_name, + cutout_position, + deck_definition, + ) + ) + return {area.area_name: area for area in addressable_areas} + + def _check_location_is_addressable_area( + self, location: Union[DeckSlotLocation, AddressableAreaLocation, str] + ) -> None: + if isinstance(location, DeckSlotLocation): + addressable_area_name = location.slotName.id + elif isinstance(location, AddressableAreaLocation): + addressable_area_name = location.addressableAreaName + else: + addressable_area_name = location + + if addressable_area_name not in self._state.loaded_addressable_areas_by_name: + # TODO Uncomment this out once robot server side stuff is hooked up + # if not self._state.use_simulated_deck_config: + # raise AreaNotInDeckConfigurationError( + # f"{addressable_area_name} not provided by deck configuration." + # ) + cutout_id = self._validate_addressable_area_for_simulation( + addressable_area_name + ) + cutout_position = deck_configuration_provider.get_cutout_position( + cutout_id, self._state.deck_definition + ) + addressable_area = ( + deck_configuration_provider.get_addressable_area_from_name( + addressable_area_name, cutout_position, self._state.deck_definition + ) + ) + self._state.loaded_addressable_areas_by_name[ + addressable_area.area_name + ] = addressable_area + + def _validate_addressable_area_for_simulation( + self, addressable_area_name: str + ) -> str: + """Given an addressable area name, validate it can exist on the deck and return cutout id associated with it.""" + ( + cutout_id, + potential_fixtures, + ) = deck_configuration_provider.get_potential_cutout_fixtures( + addressable_area_name, self._state.deck_definition + ) + + if cutout_id in self._state.potential_cutout_fixtures_by_cutout_id: + existing_potential_fixtures = ( + self._state.potential_cutout_fixtures_by_cutout_id[cutout_id] + ) + remaining_fixtures = existing_potential_fixtures.intersection( + set(potential_fixtures) + ) + if not remaining_fixtures: + loaded_areas_on_cutout = _get_conflicting_addressable_areas( + existing_potential_fixtures, + set(self.state.loaded_addressable_areas_by_name), + self._state.deck_definition, + ) + raise IncompatibleAddressableAreaError( + f"Cannot load {addressable_area_name}, not compatible with one or more of" + f" the following areas: {loaded_areas_on_cutout}" + ) + self._state.potential_cutout_fixtures_by_cutout_id[ + cutout_id + ] = remaining_fixtures + else: + self._state.potential_cutout_fixtures_by_cutout_id[cutout_id] = set( + potential_fixtures + ) + return cutout_id + + +class AddressableAreaView(HasState[AddressableAreaState]): + """Read-only addressable area state view.""" + + _state: AddressableAreaState + + def __init__(self, state: AddressableAreaState) -> None: + """Initialize the computed view of addressable area state. + + Arguments: + state: Addressable area state dataclass used for all calculations. + """ + self._state = state + + def get_addressable_area(self, addressable_area_name: str) -> AddressableArea: + """Get addressable area.""" + if not self._state.use_simulated_deck_config: + return self._get_loaded_addressable_area(addressable_area_name) + else: + return self._get_addressable_area_for_simulation(addressable_area_name) + + def get_all(self) -> List[str]: + """Get a list of all loaded addressable area names.""" + return list(self._state.loaded_addressable_areas_by_name) + + def _get_loaded_addressable_area( + self, addressable_area_name: str + ) -> AddressableArea: + """Get an addressable area that has been loaded into state. Will raise error if it does not exist.""" + try: + return self._state.loaded_addressable_areas_by_name[addressable_area_name] + except KeyError: + raise AreaNotInDeckConfigurationError( + f"{addressable_area_name} not provided by deck configuration." + ) + + def _get_addressable_area_for_simulation( + self, addressable_area_name: str + ) -> AddressableArea: + """Get an addressable area that may not have been already loaded for a simulated run. + + Since this may be the first time this addressable area has been called, and it might not exist in the store + yet (and if not won't until the result completes), we have to check if it is theoretically possible and then + get the area data from the deck configuration provider. + """ + if addressable_area_name in self._state.loaded_addressable_areas_by_name: + return self._state.loaded_addressable_areas_by_name[addressable_area_name] + + ( + cutout_id, + potential_fixtures, + ) = deck_configuration_provider.get_potential_cutout_fixtures( + addressable_area_name, self._state.deck_definition + ) + + if cutout_id in self._state.potential_cutout_fixtures_by_cutout_id: + if not self._state.potential_cutout_fixtures_by_cutout_id[ + cutout_id + ].intersection(potential_fixtures): + loaded_areas_on_cutout = _get_conflicting_addressable_areas( + self._state.potential_cutout_fixtures_by_cutout_id[cutout_id], + set(self._state.loaded_addressable_areas_by_name), + self.state.deck_definition, + ) + raise IncompatibleAddressableAreaError( + f"Cannot load {addressable_area_name}, not compatible with one or more of" + f" the following areas: {loaded_areas_on_cutout}" + ) + + cutout_position = deck_configuration_provider.get_cutout_position( + cutout_id, self._state.deck_definition + ) + return deck_configuration_provider.get_addressable_area_from_name( + addressable_area_name, cutout_position, self._state.deck_definition + ) + + def get_addressable_area_position(self, addressable_area_name: str) -> Point: + """Get the position of an addressable area.""" + # TODO This should be the regular `get_addressable_area` once Robot Server deck config and tests is hooked up + addressable_area = self._get_addressable_area_for_simulation( + addressable_area_name + ) + position = addressable_area.position + return Point(x=position.x, y=position.y, z=position.z) + + def get_addressable_area_center(self, addressable_area_name: str) -> Point: + """Get the (x, y, z) position of the center of the area.""" + addressable_area = self.get_addressable_area(addressable_area_name) + position = addressable_area.position + bounding_box = addressable_area.bounding_box + return Point( + x=position.x + bounding_box.x / 2, + y=position.y + bounding_box.y / 2, + z=position.z, + ) + + def get_addressable_area_height(self, addressable_area_name: str) -> float: + """Get the z height of an addressable area.""" + addressable_area = self.get_addressable_area(addressable_area_name) + return addressable_area.bounding_box.z + + def get_slot_definition(self, slot: DeckSlotName) -> SlotDefV3: + """Get the definition of a slot in the deck.""" + try: + # TODO This should be the regular `get_addressable_area` once Robot Server deck config and tests is hooked up + addressable_area = self._get_addressable_area_for_simulation(slot.id) + except (AreaNotInDeckConfigurationError, IncompatibleAddressableAreaError): + raise SlotDoesNotExistError( + f"Slot ID {slot.id} does not exist in deck {self._state.deck_definition['otId']}" + ) + position = addressable_area.position + bounding_box = addressable_area.bounding_box + return { + "id": addressable_area.area_name, + "position": [position.x, position.y, position.z], + "boundingBox": { + "xDimension": bounding_box.x, + "yDimension": bounding_box.y, + "zDimension": bounding_box.z, + }, + "displayName": addressable_area.display_name, + "compatibleModuleTypes": addressable_area.compatible_module_types, + } diff --git a/api/src/opentrons/protocol_engine/state/config.py b/api/src/opentrons/protocol_engine/state/config.py index f1ba812bb8f..c5ba5fb07db 100644 --- a/api/src/opentrons/protocol_engine/state/config.py +++ b/api/src/opentrons/protocol_engine/state/config.py @@ -17,10 +17,14 @@ class Config: or pretending to control. ignore_pause: The engine should no-op instead of waiting for pauses and delays to complete. + use_virtual_pipettes: The engine should no-op instead of calling + instruments' hardware control API use_virtual_modules: The engine should no-op instead of calling modules' hardware control API. use_virtual_gripper: The engine should no-op instead of calling gripper hardware control API. + use_simulated_deck_config: The engine should lazily populate the deck + configuration instead of loading a provided configuration block_on_door_open: Protocol execution should pause if the front door is opened. """ @@ -31,4 +35,5 @@ class Config: use_virtual_pipettes: bool = False use_virtual_modules: bool = False use_virtual_gripper: bool = False + use_simulated_deck_config: bool = False block_on_door_open: bool = False diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 1e739999413..8e5ba854f5f 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -7,6 +7,7 @@ from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN from .. import errors +from ..resources import fixture_validation from ..types import ( OFF_DECK_LOCATION, LoadedLabware, @@ -28,11 +29,13 @@ TipGeometry, LabwareMovementOffsetData, OnDeckLabwareLocation, + AddressableAreaLocation, ) from .config import Config from .labware import LabwareView from .modules import ModuleView from .pipettes import PipetteView +from .addressable_areas import AddressableAreaView from opentrons_shared_data.pipette import PIPETTE_X_SPAN from opentrons_shared_data.pipette.dev_types import ChannelCount @@ -69,12 +72,14 @@ def __init__( labware_view: LabwareView, module_view: ModuleView, pipette_view: PipetteView, + addressable_area_view: AddressableAreaView, ) -> None: """Initialize a GeometryView instance.""" self._config = config self._labware = labware_view self._modules = module_view self._pipettes = pipette_view + self._addressable_areas = addressable_area_view self._last_drop_tip_location_spot: Optional[_TipDropSection] = None def get_labware_highest_z(self, labware_id: str) -> float: @@ -103,7 +108,15 @@ def get_all_labware_highest_z(self) -> float: default=0.0, ) - return max(highest_labware_z, highest_module_z) + highest_addressable_area_z = max( + ( + self._addressable_areas.get_addressable_area_height(area_name) + for area_name in self._addressable_areas.get_all() + ), + default=0.0, + ) + + return max(highest_labware_z, highest_module_z, highest_addressable_area_z) def get_min_travel_z( self, @@ -128,7 +141,7 @@ def get_min_travel_z( def get_labware_parent_nominal_position(self, labware_id: str) -> Point: """Get the position of the labware's uncalibrated parent slot (deck, module, or another labware).""" slot_name = self.get_ancestor_slot_name(labware_id) - slot_pos = self._labware.get_slot_position(slot_name) + slot_pos = self._addressable_areas.get_addressable_area_position(slot_name.id) labware_data = self._labware.get(labware_id) offset = self._get_labware_position_offset(labware_id, labware_data.location) @@ -154,7 +167,7 @@ def _get_labware_position_offset( on modules as well as stacking overlaps. Does not include module calibration offset or LPC offset. """ - if isinstance(labware_location, DeckSlotLocation): + if isinstance(labware_location, (AddressableAreaLocation, DeckSlotLocation)): return LabwareOffsetVector(x=0, y=0, z=0) elif isinstance(labware_location, ModuleLocation): module_id = labware_location.moduleId @@ -230,7 +243,9 @@ def _get_calibrated_module_offset( return self._normalize_module_calibration_offset( module_location, offset_data ) - elif isinstance(location, DeckSlotLocation): + elif isinstance(location, (DeckSlotLocation, AddressableAreaLocation)): + # TODO we might want to do a check here to make sure addressable area location is a standard deck slot + # and raise if its not (or maybe we don't actually care since modules will never be loaded elsewhere) return ModuleOffsetVector(x=0, y=0, z=0) elif isinstance(location, OnLabwareLocation): labware_data = self._labware.get(location.labwareId) @@ -404,12 +419,20 @@ def get_checked_tip_drop_location( pipette_id: str, labware_id: str, well_location: DropTipWellLocation, + partially_configured: bool = False, ) -> WellLocation: """Get tip drop location given labware and hardware pipette. This makes sure that the well location has an appropriate origin & offset if one is not already set previously. """ + if ( + self._labware.get_definition(labware_id).parameters.isTiprack + and partially_configured + ): + raise errors.UnexpectedProtocolError( + "Cannot return tip to a tiprack while the pipette is configured for partial tip." + ) if well_location.origin != DropTipWellOrigin.DEFAULT: return WellLocation( origin=WellOrigin(well_location.origin.value), @@ -448,6 +471,15 @@ def get_ancestor_slot_name(self, labware_id: str) -> DeckSlotName: elif isinstance(labware.location, OnLabwareLocation): below_labware_id = labware.location.labwareId slot_name = self.get_ancestor_slot_name(below_labware_id) + elif isinstance(labware.location, AddressableAreaLocation): + area_name = labware.location.addressableAreaName + # TODO we might want to eventually return some sort of staging slot name when we're ready to work through + # the linting nightmare it will create + if fixture_validation.is_staging_slot(area_name): + raise ValueError( + "Cannot get ancestor slot name for labware on staging slot." + ) + slot_name = DeckSlotName.from_primitive(area_name) elif labware.location == OFF_DECK_LOCATION: raise errors.LabwareNotOnDeckError( f"Labware {labware_id} does not have a slot associated with it" @@ -459,9 +491,8 @@ def get_ancestor_slot_name(self, labware_id: str) -> DeckSlotName: def ensure_location_not_occupied( self, location: _LabwareLocation ) -> _LabwareLocation: - """Ensure that the location does not already have equipment in it.""" - # TODO (spp, 2023-11-07): update to include addressable areas - if isinstance(location, (DeckSlotLocation, ModuleLocation, OnLabwareLocation)): + """Ensure that the location does not already have either Labware or a Module in it.""" + if isinstance(location, (DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation)): self._labware.raise_if_labware_in_location(location) if isinstance(location, DeckSlotLocation): self._modules.raise_if_module_in_location(location) @@ -470,7 +501,9 @@ def ensure_location_not_occupied( def get_labware_grip_point( self, labware_id: str, - location: Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation], + location: Union[ + DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation + ], ) -> Point: """Get the grip point of the labware as placed on the given location. @@ -485,16 +518,33 @@ def get_labware_grip_point( grip_height_from_labware_bottom = ( self._labware.get_grip_height_from_labware_bottom(labware_id) ) - location_slot: DeckSlotName + location_name: str if isinstance(location, DeckSlotLocation): - location_slot = location.slotName + location_name = location.slotName.id offset = LabwareOffsetVector(x=0, y=0, z=0) + elif isinstance(location, AddressableAreaLocation): + location_name = location.addressableAreaName + if fixture_validation.is_gripper_waste_chute(location_name): + gripper_waste_chute = self._addressable_areas.get_addressable_area( + location_name + ) + drop_labware_location = gripper_waste_chute.drop_labware_location + if drop_labware_location is None: + raise ValueError( + f"{location_name} does not have a drop labware location associated with it" + ) + return drop_labware_location + Point(z=grip_height_from_labware_bottom) + # Location should have been pre-validated so this will be a deck/staging area slot + else: + offset = LabwareOffsetVector(x=0, y=0, z=0) else: if isinstance(location, ModuleLocation): - location_slot = self._modules.get_location(location.moduleId).slotName + location_name = self._modules.get_location( + location.moduleId + ).slotName.id else: # OnLabwareLocation - location_slot = self.get_ancestor_slot_name(location.labwareId) + location_name = self.get_ancestor_slot_name(location.labwareId).id labware_offset = self._get_labware_position_offset(labware_id, location) # Get the calibrated offset if the on labware location is on top of a module, otherwise return empty one cal_offset = self._get_calibrated_module_offset(location) @@ -504,11 +554,13 @@ def get_labware_grip_point( z=labware_offset.z + cal_offset.z, ) - slot_center = self._labware.get_slot_center_position(location_slot) + location_center = self._addressable_areas.get_addressable_area_center( + location_name + ) return Point( - slot_center.x + offset.x, - slot_center.y + offset.y, - slot_center.z + offset.z + grip_height_from_labware_bottom, + location_center.x + offset.x, + location_center.y + offset.y, + location_center.z + offset.z + grip_height_from_labware_bottom, ) def get_extra_waypoints( @@ -522,8 +574,8 @@ def get_extra_waypoints( middle_slot = DeckSlotName.SLOT_5.to_equivalent_for_robot_type( self._config.robot_type ) - middle_slot_center = self._labware.get_slot_center_position( - slot=middle_slot, + middle_slot_center = self._addressable_areas.get_addressable_area_center( + addressable_area_name=middle_slot.id, ) return [(middle_slot_center.x, middle_slot_center.y)] return [] @@ -707,10 +759,18 @@ def get_final_labware_movement_offset_vectors( @staticmethod def ensure_valid_gripper_location( location: LabwareLocation, - ) -> Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation]: + ) -> Union[ + DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation + ]: """Ensure valid on-deck location for gripper, otherwise raise error.""" if not isinstance( - location, (DeckSlotLocation, ModuleLocation, OnLabwareLocation) + location, + ( + DeckSlotLocation, + ModuleLocation, + OnLabwareLocation, + AddressableAreaLocation, + ), ): raise errors.LabwareMovementNotAllowedError( "Off-deck labware movements are not supported using the gripper." @@ -722,7 +782,9 @@ def get_total_nominal_gripper_offset_for_move_type( ) -> LabwareOffsetVector: """Get the total of the offsets to be used to pick up labware in its current location.""" if move_type == _GripperMoveType.PICK_UP_LABWARE: - if isinstance(location, (ModuleLocation, DeckSlotLocation)): + if isinstance( + location, (ModuleLocation, DeckSlotLocation, AddressableAreaLocation) + ): return self._nominal_gripper_offsets_for_location(location).pickUpOffset else: # If it's a labware on a labware (most likely an adapter), @@ -742,7 +804,9 @@ def get_total_nominal_gripper_offset_for_move_type( ).pickUpOffset ) else: - if isinstance(location, (ModuleLocation, DeckSlotLocation)): + if isinstance( + location, (ModuleLocation, DeckSlotLocation, AddressableAreaLocation) + ): return self._nominal_gripper_offsets_for_location(location).dropOffset else: # If it's a labware on a labware (most likely an adapter), @@ -766,7 +830,9 @@ def _nominal_gripper_offsets_for_location( self, location: OnDeckLabwareLocation ) -> LabwareMovementOffsetData: """Provide the default gripper offset data for the given location type.""" - if isinstance(location, DeckSlotLocation): + if isinstance(location, (DeckSlotLocation, AddressableAreaLocation)): + # TODO we might need a separate type of gripper offset for addressable areas but that also might just + # be covered by the drop labware offset/location offsets = self._labware.get_deck_default_gripper_offsets() elif isinstance(location, ModuleLocation): offsets = self._modules.get_default_gripper_offsets(location.moduleId) diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 6b67bc2f663..811f6662f26 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -14,18 +14,18 @@ cast, ) -from opentrons_shared_data.deck.dev_types import DeckDefinitionV4, SlotDefV3 +from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 from opentrons_shared_data.gripper.constants import LABWARE_GRIP_FORCE from opentrons_shared_data.labware.labware_definition import LabwareRole from opentrons_shared_data.pipette.dev_types import LabwareUri -from opentrons.types import DeckSlotName, Point, MountType +from opentrons.types import DeckSlotName, MountType from opentrons.protocols.api_support.constants import OPENTRONS_NAMESPACE from opentrons.protocols.models import LabwareDefinition, WellDefinition from opentrons.calibration_storage.helpers import uri_from_details from .. import errors -from ..resources import DeckFixedLabware, labware_validation +from ..resources import DeckFixedLabware, labware_validation, fixture_validation from ..commands import ( Command, LoadLabwareResult, @@ -34,6 +34,7 @@ from ..types import ( DeckSlotLocation, OnLabwareLocation, + AddressableAreaLocation, NonStackedLocation, Dimensions, LabwareOffset, @@ -46,6 +47,7 @@ OverlapOffset, LabwareMovementOffsetData, OnDeckLabwareLocation, + OFF_DECK_LOCATION, ) from ..actions import ( Action, @@ -202,6 +204,13 @@ def _handle_command(self, command: Command) -> None: new_offset_id = command.result.offsetId self._state.labware_by_id[labware_id].offsetId = new_offset_id + if isinstance( + new_location, AddressableAreaLocation + ) and fixture_validation.is_gripper_waste_chute( + new_location.addressableAreaName + ): + # If a labware has been moved into a waste chute it's been chuted away and is now technically off deck + new_location = OFF_DECK_LOCATION self._state.labware_by_id[labware_id].location = new_location def _add_labware_offset(self, labware_offset: LabwareOffset) -> None: @@ -304,82 +313,6 @@ def get_deck_definition(self) -> DeckDefinitionV4: """Get the current deck definition.""" return self._state.deck_definition - def get_slot_definition(self, slot: DeckSlotName) -> SlotDefV3: - """Get the definition of a slot in the deck.""" - deck_def = self.get_deck_definition() - - # TODO(jbl 2023-10-19 this is all incredibly hacky and ultimately we should get rid of SlotDefV3, and maybe - # move all this to another store/provider. However for now, this can be more or less equivalent and not break - # things TM TM TM - - for cutout in deck_def["locations"]["cutouts"]: - if cutout["id"].endswith(slot.id): - base_position = cutout["position"] - break - else: - raise errors.SlotDoesNotExistError( - f"Slot ID {slot.id} does not exist in deck {deck_def['otId']}" - ) - - slot_def: SlotDefV3 - # Slot 12/fixed trash for ot2 is a little weird so if its that just return some hardcoded stuff - if slot.id == "12": - slot_def = { - "id": "12", - "position": base_position, - "boundingBox": { - "xDimension": 128.0, - "yDimension": 86.0, - "zDimension": 0, - }, - "displayName": "Slot 12", - "compatibleModuleTypes": [], - } - return slot_def - - for area in deck_def["locations"]["addressableAreas"]: - if area["id"] == slot.id: - offset = area["offsetFromCutoutFixture"] - position = [ - offset[0] + base_position[0], - offset[1] + base_position[1], - offset[2] + base_position[2], - ] - slot_def = { - "id": area["id"], - "position": position, - "boundingBox": area["boundingBox"], - "displayName": area["displayName"], - "compatibleModuleTypes": area["compatibleModuleTypes"], - } - if area.get("matingSurfaceUnitVector"): - slot_def["matingSurfaceUnitVector"] = area[ - "matingSurfaceUnitVector" - ] - return slot_def - - raise errors.SlotDoesNotExistError( - f"Slot ID {slot.id} does not exist in deck {deck_def['otId']}" - ) - - def get_slot_position(self, slot: DeckSlotName) -> Point: - """Get the position of a deck slot.""" - slot_def = self.get_slot_definition(slot) - position = slot_def["position"] - - return Point(x=position[0], y=position[1], z=position[2]) - - def get_slot_center_position(self, slot: DeckSlotName) -> Point: - """Get the (x, y, z) position of the center of the slot.""" - slot_def = self.get_slot_definition(slot) - position = slot_def["position"] - - return Point( - x=position[0] + slot_def["boundingBox"]["xDimension"] / 2, - y=position[1] + slot_def["boundingBox"]["yDimension"] / 2, - z=position[2] + slot_def["boundingBox"]["zDimension"] / 2, - ) - def get_definition_by_uri(self, uri: LabwareUri) -> LabwareDefinition: """Get the labware definition matching loadName namespace and version.""" try: @@ -725,7 +658,9 @@ def is_fixed_trash(self, labware_id: str) -> bool: """Check if labware is fixed trash.""" return self.get_fixed_trash_id() == labware_id - def raise_if_labware_in_location(self, location: OnDeckLabwareLocation) -> None: + def raise_if_labware_in_location( + self, location: OnDeckLabwareLocation, + ) -> None: """Raise an error if the specified location has labware in it.""" for labware in self.get_all(): if labware.location == location: diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index 8c8d7a366cb..6529433e0e8 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -6,6 +6,10 @@ from opentrons_shared_data.pipette import pipette_definition from opentrons.config.defaults_ot2 import Z_RETRACT_DISTANCE from opentrons.hardware_control.dev_types import PipetteDict +from opentrons.hardware_control.nozzle_manager import ( + NozzleConfigurationType, + NozzleMap, +) from opentrons.types import MountType, Mount as HwMount from .. import errors @@ -39,7 +43,10 @@ CommandPrivateResult, PrepareToAspirateResult, ) -from ..commands.configuring_common import PipetteConfigUpdateResultMixin +from ..commands.configuring_common import ( + PipetteConfigUpdateResultMixin, + PipetteNozzleLayoutResultMixin, +) from ..actions import ( Action, SetPipetteMovementSpeedAction, @@ -94,6 +101,7 @@ class PipetteState: movement_speed_by_id: Dict[str, Optional[float]] static_config_by_id: Dict[str, StaticPipetteConfig] flow_rates_by_id: Dict[str, FlowRates] + nozzle_configuration_by_id: Dict[str, Optional[NozzleMap]] class PipetteStore(HasState[PipetteState], HandlesActions): @@ -112,6 +120,7 @@ def __init__(self) -> None: movement_speed_by_id={}, static_config_by_id={}, flow_rates_by_id={}, + nozzle_configuration_by_id={}, ) def handle_action(self, action: Action) -> None: @@ -144,6 +153,10 @@ def _handle_command( # noqa: C901 nozzle_offset_z=config.nozzle_offset_z, ) self._state.flow_rates_by_id[private_result.pipette_id] = config.flow_rates + elif isinstance(private_result, PipetteNozzleLayoutResultMixin): + self._state.nozzle_configuration_by_id[ + private_result.pipette_id + ] = private_result.nozzle_map if isinstance(command.result, LoadPipetteResult): pipette_id = command.result.pipetteId @@ -156,6 +169,7 @@ def _handle_command( # noqa: C901 self._state.aspirated_volume_by_id[pipette_id] = None self._state.movement_speed_by_id[pipette_id] = None self._state.attached_tip_by_id[pipette_id] = None + self._state.nozzle_configuration_by_id[pipette_id] = None elif isinstance(command.result, AspirateResult): pipette_id = command.params.pipetteId @@ -535,6 +549,10 @@ def get_serial_number(self, pipette_id: str) -> str: """Get the serial number of the pipette.""" return self.get_config(pipette_id).serial_number + def get_channels(self, pipette_id: str) -> int: + """Return the max channels of the pipette.""" + return self.get_config(pipette_id).channels + def get_minimum_volume(self, pipette_id: str) -> float: """Return the given pipette's minimum volume.""" return self.get_config(pipette_id).min_volume @@ -597,3 +615,15 @@ def get_plunger_axis(self, pipette_id: str) -> MotorAxis: if mount == MountType.LEFT else MotorAxis.RIGHT_PLUNGER ) + + def get_nozzle_layout_type(self, pipette_id: str) -> NozzleConfigurationType: + """Get the current set nozzle layout configuration.""" + nozzle_map_for_pipette = self._state.nozzle_configuration_by_id.get(pipette_id) + if nozzle_map_for_pipette: + return nozzle_map_for_pipette.configuration + else: + return NozzleConfigurationType.FULL + + def get_is_partially_configured(self, pipette_id: str) -> bool: + """Determine if the provided pipette is partially configured.""" + return self.get_nozzle_layout_type(pipette_id) != NozzleConfigurationType.FULL diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index 3c402701810..5a273085070 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -14,6 +14,11 @@ from .abstract_store import HasState, HandlesActions from .change_notifier import ChangeNotifier from .commands import CommandState, CommandStore, CommandView +from .addressable_areas import ( + AddressableAreaState, + AddressableAreaStore, + AddressableAreaView, +) from .labware import LabwareState, LabwareStore, LabwareView from .pipettes import PipetteState, PipetteStore, PipetteView from .modules import ModuleState, ModuleStore, ModuleView @@ -23,6 +28,7 @@ from .motion import MotionView from .config import Config from .state_summary import StateSummary +from ..types import DeckConfigurationType ReturnT = TypeVar("ReturnT") @@ -32,6 +38,7 @@ class State: """Underlying engine state.""" commands: CommandState + addressable_areas: AddressableAreaState labware: LabwareState pipettes: PipetteState modules: ModuleState @@ -44,6 +51,7 @@ class StateView(HasState[State]): _state: State _commands: CommandView + _addressable_areas: AddressableAreaView _labware: LabwareView _pipettes: PipetteView _modules: ModuleView @@ -58,6 +66,11 @@ def commands(self) -> CommandView: """Get state view selectors for commands state.""" return self._commands + @property + def addressable_areas(self) -> AddressableAreaView: + """Get state view selectors for addressable area state.""" + return self._addressable_areas + @property def labware(self) -> LabwareView: """Get state view selectors for labware state.""" @@ -101,6 +114,7 @@ def config(self) -> Config: def get_summary(self) -> StateSummary: """Get protocol run data.""" error = self._commands.get_error() + # TODO maybe add summary here for AA return StateSummary.construct( status=self._commands.get_status(), errors=[] if error is None else [error], @@ -131,6 +145,7 @@ def __init__( is_door_open: bool, change_notifier: Optional[ChangeNotifier] = None, module_calibration_offsets: Optional[Dict[str, ModuleOffsetData]] = None, + deck_configuration: Optional[DeckConfigurationType] = None, ) -> None: """Initialize a StateStore and its substores. @@ -143,9 +158,17 @@ def __init__( is_door_open: Whether the robot's door is currently open. change_notifier: Internal state change notifier. module_calibration_offsets: Module offsets to preload. + deck_configuration: The initial deck configuration the addressable area store will be instantiated with. """ self._command_store = CommandStore(config=config, is_door_open=is_door_open) self._pipette_store = PipetteStore() + if deck_configuration is None: + deck_configuration = [] + self._addressable_area_store = AddressableAreaStore( + deck_configuration=deck_configuration, + config=config, + deck_definition=deck_definition, + ) self._labware_store = LabwareStore( deck_fixed_labware=deck_fixed_labware, deck_definition=deck_definition, @@ -159,6 +182,7 @@ def __init__( self._substores: List[HandlesActions] = [ self._command_store, self._pipette_store, + self._addressable_area_store, self._labware_store, self._module_store, self._liquid_store, @@ -243,6 +267,7 @@ def _get_next_state(self) -> State: """Get a new instance of the state value object.""" return State( commands=self._command_store.state, + addressable_areas=self._addressable_area_store.state, labware=self._labware_store.state, pipettes=self._pipette_store.state, modules=self._module_store.state, @@ -257,6 +282,7 @@ def _initialize_state(self) -> None: # Base states self._state = state self._commands = CommandView(state.commands) + self._addressable_areas = AddressableAreaView(state.addressable_areas) self._labware = LabwareView(state.labware) self._pipettes = PipetteView(state.pipettes) self._modules = ModuleView(state.modules) @@ -269,6 +295,7 @@ def _initialize_state(self) -> None: labware_view=self._labware, module_view=self._modules, pipette_view=self._pipettes, + addressable_area_view=self._addressable_areas, ) self._motion = MotionView( config=self._config, @@ -283,6 +310,7 @@ def _update_state_views(self) -> None: next_state = self._get_next_state() self._state = next_state self._commands._state = next_state.commands + self._addressable_areas._state = next_state.addressable_areas self._labware._state = next_state.labware self._pipettes._state = next_state.pipettes self._modules._state = next_state.modules diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 228cb066aaf..0ddd8ae96c0 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -5,11 +5,11 @@ from enum import Enum from dataclasses import dataclass from pydantic import BaseModel, Field, validator -from typing import Optional, Union, List, Dict, Any, NamedTuple +from typing import Optional, Union, List, Dict, Any, NamedTuple, Tuple from typing_extensions import Literal, TypeGuard from opentrons_shared_data.pipette.dev_types import PipetteNameType -from opentrons.types import MountType, DeckSlotName +from opentrons.types import MountType, DeckSlotName, Point from opentrons.hardware_control.modules import ( ModuleType as ModuleType, ) @@ -18,6 +18,7 @@ # convenience re-export of LabwareUri type LabwareUri as LabwareUri, ) +from opentrons_shared_data.module.dev_types import ModuleType as SharedDataModuleType class EngineStatus(str, Enum): @@ -54,6 +55,19 @@ class DeckSlotLocation(BaseModel): ) +class AddressableAreaLocation(BaseModel): + """The location of something place in an addressable area. This is a superset of deck slots.""" + + addressableAreaName: str = Field( + ..., + description=( + "The name of the addressable area that you want to use." + " Valid values are the `id`s of `addressableArea`s in the" + " [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck)." + ), + ) + + class ModuleLocation(BaseModel): """The location of something placed atop a hardware module.""" @@ -76,13 +90,21 @@ class OnLabwareLocation(BaseModel): OFF_DECK_LOCATION: _OffDeckLocationType = "offDeck" LabwareLocation = Union[ - DeckSlotLocation, ModuleLocation, OnLabwareLocation, _OffDeckLocationType + DeckSlotLocation, + ModuleLocation, + OnLabwareLocation, + _OffDeckLocationType, + AddressableAreaLocation, ] """Union of all locations where it's legal to keep a labware.""" -OnDeckLabwareLocation = Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation] +OnDeckLabwareLocation = Union[ + DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation +] -NonStackedLocation = Union[DeckSlotLocation, ModuleLocation, _OffDeckLocationType] +NonStackedLocation = Union[ + DeckSlotLocation, AddressableAreaLocation, ModuleLocation, _OffDeckLocationType +] """Union of all locations where it's legal to keep a labware that can't be stacked on another labware""" @@ -390,6 +412,10 @@ class OverlapOffset(Vec3f): """Offset representing overlap space of one labware on top of another labware or module.""" +class AddressableOffsetVector(Vec3f): + """Offset, in deck coordinates, from nominal to actual position of an addressable area.""" + + class LabwareMovementOffsetData(BaseModel): """Offsets to be used during labware movement.""" @@ -637,6 +663,28 @@ class LabwareMovementStrategy(str, Enum): MANUAL_MOVE_WITHOUT_PAUSE = "manualMoveWithoutPause" +@dataclass(frozen=True) +class PotentialCutoutFixture: + """Cutout and cutout fixture id associated with a potential cutout fixture that can be on the deck.""" + + cutout_id: str + cutout_fixture_id: str + + +@dataclass(frozen=True) +class AddressableArea: + """Addressable area that has been loaded.""" + + area_name: str + display_name: str + bounding_box: Dimensions + position: AddressableOffsetVector + compatible_module_types: List[SharedDataModuleType] + # TODO do we need "ableToDropLabware" in the definition? + drop_tip_location: Optional[Point] + drop_labware_location: Optional[Point] + + class PostRunHardwareState(Enum): """State of robot gantry & motors after a stop is performed and the hardware API is reset. @@ -662,3 +710,70 @@ class PostRunHardwareState(Enum): HOME_THEN_DISENGAGE = "homeThenDisengage" STAY_ENGAGED_IN_PLACE = "stayEngagedInPlace" DISENGAGE_IN_PLACE = "disengageInPlace" + + +NOZZLE_NAME_REGEX = "[A-Z][0-100]" +PRIMARY_NOZZLE_LITERAL = Literal["A1", "H1", "A12", "H12"] + + +class EmptyNozzleLayoutConfiguration(BaseModel): + """Empty basemodel to represent a reset to the nozzle configuration. Sending no parameters resets to default.""" + + style: Literal["EMPTY"] = "EMPTY" + + +class SingleNozzleLayoutConfiguration(BaseModel): + """Minimum information required for a new nozzle configuration.""" + + style: Literal["SINGLE"] = "SINGLE" + primary_nozzle: PRIMARY_NOZZLE_LITERAL = Field( + ..., + description="The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + ) + + +class RowNozzleLayoutConfiguration(BaseModel): + """Minimum information required for a new nozzle configuration.""" + + style: Literal["ROW"] = "ROW" + primary_nozzle: PRIMARY_NOZZLE_LITERAL = Field( + ..., + description="The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + ) + + +class ColumnNozzleLayoutConfiguration(BaseModel): + """Information required for nozzle configurations of type ROW and COLUMN.""" + + style: Literal["COLUMN"] = "COLUMN" + primary_nozzle: PRIMARY_NOZZLE_LITERAL = Field( + ..., + description="The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + ) + + +class QuadrantNozzleLayoutConfiguration(BaseModel): + """Information required for nozzle configurations of type QUADRANT.""" + + style: Literal["QUADRANT"] = "QUADRANT" + primary_nozzle: PRIMARY_NOZZLE_LITERAL = Field( + ..., + description="The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + ) + front_right_nozzle: str = Field( + ..., + regex=NOZZLE_NAME_REGEX, + description="The front right nozzle in your configuration.", + ) + + +NozzleLayoutConfigurationType = Union[ + EmptyNozzleLayoutConfiguration, + SingleNozzleLayoutConfiguration, + ColumnNozzleLayoutConfiguration, + RowNozzleLayoutConfiguration, + QuadrantNozzleLayoutConfiguration, +] + +# TODO make the below some sort of better type +DeckConfigurationType = List[Tuple[str, str]] diff --git a/api/src/opentrons/protocol_runner/create_simulating_runner.py b/api/src/opentrons/protocol_runner/create_simulating_runner.py index ff4df1020f7..0c60af3a45c 100644 --- a/api/src/opentrons/protocol_runner/create_simulating_runner.py +++ b/api/src/opentrons/protocol_runner/create_simulating_runner.py @@ -57,6 +57,7 @@ async def create_simulating_runner( ignore_pause=True, use_virtual_modules=True, use_virtual_gripper=True, + use_simulated_deck_config=True, use_virtual_pipettes=(not feature_flags.disable_fast_protocol_upload()), ), load_fixed_trash=should_load_fixed_trash(protocol_config), diff --git a/api/src/opentrons/protocol_runner/json_file_reader.py b/api/src/opentrons/protocol_runner/json_file_reader.py index 17f82ec8cee..488c28d273b 100644 --- a/api/src/opentrons/protocol_runner/json_file_reader.py +++ b/api/src/opentrons/protocol_runner/json_file_reader.py @@ -25,7 +25,7 @@ def read( message=f"Cannot execute {name} as a JSON protocol", detail={ "kind": "non-json-file-in-json-file-reader", - "metadata-name": protocol_source.metadata.get("name"), + "metadata-name": str(protocol_source.metadata.get("name")), "file-name": protocol_source.main_file.name, }, ) @@ -40,7 +40,9 @@ def read( message=f"{name} is a JSON protocol v{protocol_source.config.schema_version} which this robot cannot execute", detail={ "kind": "schema-version-unknown", - "requested-schema-version": protocol_source.config.schema_version, + "requested-schema-version": str( + protocol_source.config.schema_version + ), "minimum-handled-schema-version": "6", "maximum-handled-shcema-version": "8", }, diff --git a/api/src/opentrons/protocol_runner/protocol_runner.py b/api/src/opentrons/protocol_runner/protocol_runner.py index 56669077efb..eb4225a02b9 100644 --- a/api/src/opentrons/protocol_runner/protocol_runner.py +++ b/api/src/opentrons/protocol_runner/protocol_runner.py @@ -35,7 +35,7 @@ LegacyExecutor, LegacyLoadInfo, ) -from ..protocol_engine.types import PostRunHardwareState +from ..protocol_engine.types import PostRunHardwareState, DeckConfigurationType class RunResult(NamedTuple): @@ -82,9 +82,9 @@ def was_started(self) -> bool: """ return self._protocol_engine.state_view.commands.has_been_played() - def play(self) -> None: + def play(self, deck_configuration: Optional[DeckConfigurationType] = None) -> None: """Start or resume the run.""" - self._protocol_engine.play() + self._protocol_engine.play(deck_configuration=deck_configuration) def pause(self) -> None: """Pause the run.""" @@ -104,6 +104,7 @@ async def stop(self) -> None: @abstractmethod async def run( self, + deck_configuration: DeckConfigurationType, protocol_source: Optional[ProtocolSource] = None, ) -> RunResult: """Run a given protocol to completion.""" @@ -183,6 +184,7 @@ async def load( async def run( # noqa: D102 self, + deck_configuration: DeckConfigurationType, protocol_source: Optional[ProtocolSource] = None, python_parse_mode: PythonParseMode = PythonParseMode.NORMAL, ) -> RunResult: @@ -193,7 +195,7 @@ async def run( # noqa: D102 protocol_source=protocol_source, python_parse_mode=python_parse_mode ) - self.play() + self.play(deck_configuration=deck_configuration) self._task_queue.start() await self._task_queue.join() @@ -278,6 +280,7 @@ async def load(self, protocol_source: ProtocolSource) -> None: async def run( # noqa: D102 self, + deck_configuration: DeckConfigurationType, protocol_source: Optional[ProtocolSource] = None, ) -> RunResult: # TODO(mc, 2022-01-11): move load to runner creation, remove from `run` @@ -285,7 +288,7 @@ async def run( # noqa: D102 if protocol_source: await self.load(protocol_source) - self.play() + self.play(deck_configuration=deck_configuration) self._task_queue.start() await self._task_queue.join() @@ -318,11 +321,12 @@ def prepare(self) -> None: async def run( # noqa: D102 self, + deck_configuration: DeckConfigurationType, protocol_source: Optional[ProtocolSource] = None, ) -> RunResult: assert protocol_source is None await self._hardware_api.home() - self.play() + self.play(deck_configuration=deck_configuration) self._task_queue.start() await self._task_queue.join() diff --git a/api/src/opentrons/simulate.py b/api/src/opentrons/simulate.py index 2dc744432c0..24ccaf824d5 100644 --- a/api/src/opentrons/simulate.py +++ b/api/src/opentrons/simulate.py @@ -900,7 +900,9 @@ async def run(protocol_source: ProtocolSource) -> _SimulateResult: scraper = _CommandScraper(stack_logger, log_level, protocol_runner.broker) with scraper.scrape(): - result = await protocol_runner.run(protocol_source) + result = await protocol_runner.run( + deck_configuration=[], protocol_source=protocol_source + ) if result.state_summary.status != EngineStatus.SUCCEEDED: raise entrypoint_util.ProtocolEngineExecuteError( @@ -927,6 +929,7 @@ def _get_protocol_engine_config(robot_type: RobotType) -> Config: use_virtual_pipettes=True, use_virtual_modules=True, use_virtual_gripper=True, + use_simulated_deck_config=True, ) diff --git a/api/src/opentrons/types.py b/api/src/opentrons/types.py index fbfe2dab403..6c8eb06f027 100644 --- a/api/src/opentrons/types.py +++ b/api/src/opentrons/types.py @@ -364,3 +364,4 @@ class TransferTipPolicy(enum.Enum): DeckLocation = Union[int, str] +ALLOWED_PRIMARY_NOZZLES = ["A1", "H1", "A12", "H12"] diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index 979fe9b936a..f28a0723b01 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -308,6 +308,8 @@ def _make_ot3_pe_ctx( use_virtual_pipettes=True, use_virtual_modules=True, use_virtual_gripper=True, + # TODO figure out if we will want to use a "real" deck config here or if we are fine with simulated + use_simulated_deck_config=True, block_on_door_open=False, ), drop_tips_after_run=False, diff --git a/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py b/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py new file mode 100644 index 00000000000..ca963355cb2 --- /dev/null +++ b/api/tests/opentrons/hardware_control/instruments/test_nozzle_manager.py @@ -0,0 +1,837 @@ +import pytest +from typing import Dict, List, Tuple, Union, Iterator + +from opentrons.hardware_control import nozzle_manager + +from opentrons.types import Point + +from opentrons_shared_data.pipette.load_data import load_definition +from opentrons_shared_data.pipette.types import ( + PipetteModelType, + PipetteChannelType, + PipetteVersionType, +) +from opentrons_shared_data.pipette.pipette_definition import PipetteConfigurations + + +@pytest.mark.parametrize( + "pipette_details", + [ + (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)), + (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)), + (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)), + (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)), + (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)), + ], +) +def test_single_pipettes_always_full( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.SINGLE_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + + subject.update_nozzle_configuration("A1", "A1", "A1") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + + subject.reset_to_default_configuration() + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + + +@pytest.mark.parametrize( + "pipette_details", + [ + (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)), + (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)), + (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)), + (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)), + (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)), + ], +) +def test_single_pipette_map_entries( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.SINGLE_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + + def test_map_entries(nozzlemap: nozzle_manager.NozzleMap) -> None: + assert nozzlemap.back_left == "A1" + assert nozzlemap.front_right == "A1" + assert list(nozzlemap.map_store.keys()) == ["A1"] + assert list(nozzlemap.rows.keys()) == ["A"] + assert list(nozzlemap.columns.keys()) == ["1"] + assert nozzlemap.rows["A"] == ["A1"] + assert nozzlemap.columns["1"] == ["A1"] + assert nozzlemap.tip_count == 1 + + test_map_entries(subject.current_configuration) + subject.update_nozzle_configuration("A1", "A1", "A1") + test_map_entries(subject.current_configuration) + subject.reset_to_default_configuration() + test_map_entries(subject.current_configuration) + + +@pytest.mark.parametrize( + "pipette_details", + [ + (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)), + (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)), + (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)), + (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)), + (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)), + ], +) +def test_single_pipette_map_geometry( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.SINGLE_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + + def test_map_geometry(nozzlemap: nozzle_manager.NozzleMap) -> None: + assert nozzlemap.xy_center_offset == Point(*config.nozzle_map["A1"]) + assert nozzlemap.front_nozzle_offset == Point(*config.nozzle_map["A1"]) + assert nozzlemap.starting_nozzle_offset == Point(*config.nozzle_map["A1"]) + + test_map_geometry(subject.current_configuration) + subject.update_nozzle_configuration("A1", "A1", "A1") + test_map_geometry(subject.current_configuration) + subject.reset_to_default_configuration() + test_map_geometry(subject.current_configuration) + + +@pytest.mark.parametrize( + "pipette_details", + [ + (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)), + (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)), + (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)), + (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)), + (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)), + ], +) +def test_multi_config_identification( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.EIGHT_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + + subject.update_nozzle_configuration("A1", "H1", "A1") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + + subject.reset_to_default_configuration() + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + + subject.update_nozzle_configuration("A1", "D1", "A1") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.COLUMN + ) + + subject.update_nozzle_configuration("A1", "A1", "A1") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SINGLE + ) + + subject.update_nozzle_configuration("H1", "H1", "H1") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SINGLE + ) + + subject.update_nozzle_configuration("C1", "F1", "C1") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.COLUMN + ) + + subject.reset_to_default_configuration() + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + + +@pytest.mark.parametrize( + "pipette_details", + [ + (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)), + (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)), + (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)), + (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)), + (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)), + ], +) +def test_multi_config_map_entries( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.EIGHT_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + + def test_map_entries( + nozzlemap: nozzle_manager.NozzleMap, nozzles: List[str] + ) -> None: + assert nozzlemap.back_left == nozzles[0] + assert nozzlemap.front_right == nozzles[-1] + assert list(nozzlemap.map_store.keys()) == nozzles + assert list(nozzlemap.rows.keys()) == [nozzle[0] for nozzle in nozzles] + assert list(nozzlemap.columns.keys()) == ["1"] + for rowname, row_elements in nozzlemap.rows.items(): + assert row_elements == [f"{rowname}1"] + + assert nozzlemap.columns["1"] == nozzles + assert nozzlemap.tip_count == len(nozzles) + + test_map_entries( + subject.current_configuration, ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + ) + subject.update_nozzle_configuration("A1", "H1", "A1") + test_map_entries( + subject.current_configuration, ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + ) + subject.update_nozzle_configuration("A1", "D1", "A1") + test_map_entries(subject.current_configuration, ["A1", "B1", "C1", "D1"]) + subject.update_nozzle_configuration("A1", "A1", "A1") + test_map_entries(subject.current_configuration, ["A1"]) + subject.update_nozzle_configuration("H1", "H1", "H1") + test_map_entries(subject.current_configuration, ["H1"]) + subject.update_nozzle_configuration("C1", "F1", "C1") + test_map_entries(subject.current_configuration, ["C1", "D1", "E1", "F1"]) + + +@pytest.mark.parametrize( + "pipette_details", + [ + (PipetteModelType.p10, PipetteVersionType(major=1, minor=3)), + (PipetteModelType.p20, PipetteVersionType(major=2, minor=0)), + (PipetteModelType.p50, PipetteVersionType(major=3, minor=4)), + (PipetteModelType.p300, PipetteVersionType(major=2, minor=1)), + (PipetteModelType.p1000, PipetteVersionType(major=3, minor=5)), + ], +) +def test_multi_config_geometry( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.EIGHT_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + + def test_map_geometry( + nozzlemap: nozzle_manager.NozzleMap, + front_nozzle: str, + starting_nozzle: str, + xy_center_in_center_of: Union[Tuple[str, str], str], + ) -> None: + if isinstance(xy_center_in_center_of, str): + assert nozzlemap.xy_center_offset == Point( + *config.nozzle_map[xy_center_in_center_of] + ) + else: + assert nozzlemap.xy_center_offset == ( + ( + Point(*config.nozzle_map[xy_center_in_center_of[0]]) + + Point(*config.nozzle_map[xy_center_in_center_of[1]]) + ) + * 0.5 + ) + assert nozzlemap.front_nozzle_offset == Point(*config.nozzle_map[front_nozzle]) + assert nozzlemap.starting_nozzle_offset == Point( + *config.nozzle_map[starting_nozzle] + ) + + test_map_geometry(subject.current_configuration, "H1", "A1", ("A1", "H1")) + + subject.update_nozzle_configuration("A1", "A1", "A1") + test_map_geometry(subject.current_configuration, "A1", "A1", "A1") + + subject.update_nozzle_configuration("D1", "D1", "D1") + test_map_geometry(subject.current_configuration, "D1", "D1", "D1") + + subject.update_nozzle_configuration("C1", "G1", "C1") + test_map_geometry(subject.current_configuration, "G1", "C1", "E1") + + subject.update_nozzle_configuration("E1", "H1", "E1") + test_map_geometry(subject.current_configuration, "H1", "E1", ("E1", "H1")) + + subject.reset_to_default_configuration() + test_map_geometry(subject.current_configuration, "H1", "A1", ("A1", "H1")) + + +@pytest.mark.parametrize( + "pipette_details", [(PipetteModelType.p1000, PipetteVersionType(major=3, minor=5))] +) +def test_96_config_identification( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.NINETY_SIX_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + subject.update_nozzle_configuration("A1", "H12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.FULL + ) + subject.update_nozzle_configuration("A1", "H1") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.COLUMN + ) + subject.update_nozzle_configuration("A12", "H12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.COLUMN + ) + subject.update_nozzle_configuration("A8", "H8") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.COLUMN + ) + + subject.update_nozzle_configuration("A1", "A12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.ROW + ) + subject.update_nozzle_configuration("H1", "H12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.ROW + ) + subject.update_nozzle_configuration("D1", "D12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.ROW + ) + + subject.update_nozzle_configuration("E1", "H6") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SUBRECT + ) + subject.update_nozzle_configuration("E7", "H12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SUBRECT + ) + + subject.update_nozzle_configuration("C4", "F9") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SUBRECT + ) + subject.update_nozzle_configuration("A1", "D12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SUBRECT + ) + subject.update_nozzle_configuration("E1", "H12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SUBRECT + ) + subject.update_nozzle_configuration("A1", "H6") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SUBRECT + ) + subject.update_nozzle_configuration("A7", "H12") + assert ( + subject.current_configuration.configuration + == nozzle_manager.NozzleConfigurationType.SUBRECT + ) + + +@pytest.mark.parametrize( + "pipette_details", [(PipetteModelType.p1000, PipetteVersionType(major=3, minor=5))] +) +def test_96_config_map_entries( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.NINETY_SIX_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + + def test_map_entries( + nozzlemap: nozzle_manager.NozzleMap, + rows: Dict[str, List[str]], + cols: Dict[str, List[str]], + ) -> None: + assert nozzlemap.back_left == next(iter(rows.values()))[0] + assert nozzlemap.front_right == next(reversed(list(rows.values())))[-1] + + def _nozzles() -> Iterator[str]: + for row in rows.values(): + for nozzle in row: + yield nozzle + + assert list(nozzlemap.map_store.keys()) == list(_nozzles()) + assert nozzlemap.rows == rows + assert nozzlemap.columns == cols + assert nozzlemap.tip_count == sum(len(row) for row in rows.values()) + + test_map_entries( + subject.current_configuration, + { + "A": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12", + ], + "B": [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12", + ], + "C": [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12", + ], + "D": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12", + ], + "E": [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12", + ], + "F": [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12", + ], + "G": [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12", + ], + "H": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12", + ], + }, + { + "1": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + "2": ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + "3": ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + "4": ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + "5": ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + "6": ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + "7": ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + "8": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + "9": ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + "10": ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + "11": ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + "12": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"], + }, + ) + + subject.update_nozzle_configuration("A1", "H1") + test_map_entries( + subject.current_configuration, + { + "A": ["A1"], + "B": ["B1"], + "C": ["C1"], + "D": ["D1"], + "E": ["E1"], + "F": ["F1"], + "G": ["G1"], + "H": ["H1"], + }, + {"1": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"]}, + ) + + subject.update_nozzle_configuration("A12", "H12") + test_map_entries( + subject.current_configuration, + { + "A": ["A12"], + "B": ["B12"], + "C": ["C12"], + "D": ["D12"], + "E": ["E12"], + "F": ["F12"], + "G": ["G12"], + "H": ["H12"], + }, + {"12": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"]}, + ) + + subject.update_nozzle_configuration("A8", "H8") + test_map_entries( + subject.current_configuration, + { + "A": ["A8"], + "B": ["B8"], + "C": ["C8"], + "D": ["D8"], + "E": ["E8"], + "F": ["F8"], + "G": ["G8"], + "H": ["H8"], + }, + {"8": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"]}, + ) + + subject.update_nozzle_configuration("A1", "A12") + test_map_entries( + subject.current_configuration, + { + "A": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12", + ] + }, + { + "1": ["A1"], + "2": ["A2"], + "3": ["A3"], + "4": ["A4"], + "5": ["A5"], + "6": ["A6"], + "7": ["A7"], + "8": ["A8"], + "9": ["A9"], + "10": ["A10"], + "11": ["A11"], + "12": ["A12"], + }, + ) + + subject.update_nozzle_configuration("H1", "H12") + test_map_entries( + subject.current_configuration, + { + "H": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12", + ] + }, + { + "1": ["H1"], + "2": ["H2"], + "3": ["H3"], + "4": ["H4"], + "5": ["H5"], + "6": ["H6"], + "7": ["H7"], + "8": ["H8"], + "9": ["H9"], + "10": ["H10"], + "11": ["H11"], + "12": ["H12"], + }, + ) + subject.update_nozzle_configuration("D1", "D12") + test_map_entries( + subject.current_configuration, + { + "D": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12", + ] + }, + { + "1": ["D1"], + "2": ["D2"], + "3": ["D3"], + "4": ["D4"], + "5": ["D5"], + "6": ["D6"], + "7": ["D7"], + "8": ["D8"], + "9": ["D9"], + "10": ["D10"], + "11": ["D11"], + "12": ["D12"], + }, + ) + + subject.update_nozzle_configuration("A1", "D6") + test_map_entries( + subject.current_configuration, + { + "A": ["A1", "A2", "A3", "A4", "A5", "A6"], + "B": ["B1", "B2", "B3", "B4", "B5", "B6"], + "C": ["C1", "C2", "C3", "C4", "C5", "C6"], + "D": ["D1", "D2", "D3", "D4", "D5", "D6"], + }, + { + "1": ["A1", "B1", "C1", "D1"], + "2": ["A2", "B2", "C2", "D2"], + "3": ["A3", "B3", "C3", "D3"], + "4": ["A4", "B4", "C4", "D4"], + "5": ["A5", "B5", "C5", "D5"], + "6": ["A6", "B6", "C6", "D6"], + }, + ) + + subject.update_nozzle_configuration("A7", "D12") + test_map_entries( + subject.current_configuration, + { + "A": ["A7", "A8", "A9", "A10", "A11", "A12"], + "B": ["B7", "B8", "B9", "B10", "B11", "B12"], + "C": ["C7", "C8", "C9", "C10", "C11", "C12"], + "D": ["D7", "D8", "D9", "D10", "D11", "D12"], + }, + { + "7": ["A7", "B7", "C7", "D7"], + "8": ["A8", "B8", "C8", "D8"], + "9": ["A9", "B9", "C9", "D9"], + "10": ["A10", "B10", "C10", "D10"], + "11": ["A11", "B11", "C11", "D11"], + "12": ["A12", "B12", "C12", "D12"], + }, + ) + + subject.update_nozzle_configuration("E1", "H6") + test_map_entries( + subject.current_configuration, + { + "E": ["E1", "E2", "E3", "E4", "E5", "E6"], + "F": ["F1", "F2", "F3", "F4", "F5", "F6"], + "G": ["G1", "G2", "G3", "G4", "G5", "G6"], + "H": ["H1", "H2", "H3", "H4", "H5", "H6"], + }, + { + "1": ["E1", "F1", "G1", "H1"], + "2": ["E2", "F2", "G2", "H2"], + "3": ["E3", "F3", "G3", "H3"], + "4": ["E4", "F4", "G4", "H4"], + "5": ["E5", "F5", "G5", "H5"], + "6": ["E6", "F6", "G6", "H6"], + }, + ) + + subject.update_nozzle_configuration("E7", "H12") + test_map_entries( + subject.current_configuration, + { + "E": ["E7", "E8", "E9", "E10", "E11", "E12"], + "F": ["F7", "F8", "F9", "F10", "F11", "F12"], + "G": ["G7", "G8", "G9", "G10", "G11", "G12"], + "H": ["H7", "H8", "H9", "H10", "H11", "H12"], + }, + { + "7": ["E7", "F7", "G7", "H7"], + "8": ["E8", "F8", "G8", "H8"], + "9": ["E9", "F9", "G9", "H9"], + "10": ["E10", "F10", "G10", "H10"], + "11": ["E11", "F11", "G11", "H11"], + "12": ["E12", "F12", "G12", "H12"], + }, + ) + + subject.update_nozzle_configuration("C4", "D5") + test_map_entries( + subject.current_configuration, + {"C": ["C4", "C5"], "D": ["D4", "D5"]}, + {"4": ["C4", "D4"], "5": ["C5", "D5"]}, + ) + + +@pytest.mark.parametrize( + "pipette_details", [(PipetteModelType.p1000, PipetteVersionType(major=3, minor=5))] +) +def test_96_config_geometry( + pipette_details: Tuple[PipetteModelType, PipetteVersionType] +) -> None: + config = load_definition( + pipette_details[0], PipetteChannelType.NINETY_SIX_CHANNEL, pipette_details[1] + ) + subject = nozzle_manager.NozzleConfigurationManager.build_from_config(config) + + def test_map_geometry( + config: PipetteConfigurations, + nozzlemap: nozzle_manager.NozzleMap, + starting_nozzle: str, + front_nozzle: str, + center_between: Union[str, Tuple[str, str]], + ) -> None: + if isinstance(center_between, str): + assert nozzlemap.xy_center_offset == Point( + *config.nozzle_map[center_between] + ) + else: + assert ( + nozzlemap.xy_center_offset + == ( + Point(*config.nozzle_map[center_between[0]]) + + Point(*config.nozzle_map[center_between[1]]) + ) + * 0.5 + ) + + assert nozzlemap.front_nozzle_offset == Point(*config.nozzle_map[front_nozzle]) + assert nozzlemap.starting_nozzle_offset == Point( + *config.nozzle_map[starting_nozzle] + ) + + test_map_geometry(config, subject.current_configuration, "A1", "H1", ("A1", "H12")) + + subject.update_nozzle_configuration("A1", "H1") + test_map_geometry(config, subject.current_configuration, "A1", "H1", ("A1", "H1")) + + subject.update_nozzle_configuration("A12", "H12") + test_map_geometry( + config, subject.current_configuration, "A12", "H12", ("A12", "H12") + ) + + subject.update_nozzle_configuration("A1", "A12") + test_map_geometry(config, subject.current_configuration, "A1", "A1", ("A1", "A12")) + + subject.update_nozzle_configuration("H1", "H12") + test_map_geometry(config, subject.current_configuration, "H1", "H1", ("H1", "H12")) + + subject.update_nozzle_configuration("A1", "D6") + test_map_geometry(config, subject.current_configuration, "A1", "D1", ("A1", "D6")) + + subject.update_nozzle_configuration("E7", "H12") + test_map_geometry(config, subject.current_configuration, "E7", "H7", ("E7", "H12")) + + subject.update_nozzle_configuration("C4", "D5") + test_map_geometry(config, subject.current_configuration, "C4", "D4", ("C4", "D5")) diff --git a/api/tests/opentrons/hardware_control/test_ot3_api.py b/api/tests/opentrons/hardware_control/test_ot3_api.py index cd6f383c2cd..9c92d5b936f 100644 --- a/api/tests/opentrons/hardware_control/test_ot3_api.py +++ b/api/tests/opentrons/hardware_control/test_ot3_api.py @@ -30,6 +30,7 @@ TipActionSpec, TipActionMoveSpec, ) +from opentrons.hardware_control.instruments.ot3.pipette import Pipette from opentrons.hardware_control.types import ( OT3Mount, Axis, @@ -44,6 +45,7 @@ EstopStateNotification, TipStateType, ) +from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType from opentrons.hardware_control.errors import InvalidCriticalPoint from opentrons.hardware_control.ot3api import OT3API from opentrons.hardware_control import ThreadManager @@ -531,7 +533,9 @@ async def test_pickup_moves( gantry_load = GantryLoad.LOW_THROUGHPUT await ot3_hardware.set_gantry_load(gantry_load) - + pipette_handler.get_pipette( + OT3Mount.LEFT + ).nozzle_manager.current_configuration.configuration = NozzleConfigurationType.FULL z_tiprack_distance = 8.0 end_z_retract_dist = 9.0 move_plan_return_val = TipActionSpec( @@ -1457,6 +1461,7 @@ async def test_save_instrument_offset( ) +@pytest.mark.xfail() async def test_pick_up_tip_full_tiprack( ot3_hardware: ThreadManager[OT3API], mock_instrument_handlers: Tuple[Mock], @@ -1469,11 +1474,16 @@ async def test_pick_up_tip_full_tiprack( await ot3_hardware.home() _, pipette_handler = mock_instrument_handlers backend = ot3_hardware.managed_obj._backend - + instr_mock = AsyncMock(spec=Pipette) + instr_mock.nozzle_manager.current_configruation.configuration.return_value = ( + NozzleConfigurationType.FULL + ) with patch.object( backend, "tip_action", AsyncMock(spec=backend.tip_action) ) as tip_action: backend._gear_motor_position = {NodeId: 0} + pipette_handler.get_pipette.return_value = instr_mock + pipette_handler.plan_ht_pick_up_tip.return_value = TipActionSpec( shake_off_moves=[], tip_action_moves=[ diff --git a/api/tests/opentrons/hardware_control/test_pipette.py b/api/tests/opentrons/hardware_control/test_pipette.py index 582e95b589e..c6b298c51c8 100644 --- a/api/tests/opentrons/hardware_control/test_pipette.py +++ b/api/tests/opentrons/hardware_control/test_pipette.py @@ -166,7 +166,7 @@ def test_critical_points_pipette_offset( ) -> None: hw_pipette = pipette_builder(model, calibration) # pipette offset + nozzle offset to determine critical point - offsets = calibration.offset + Point(*hw_pipette.nozzle_offset) + offsets = calibration.offset + hw_pipette.nozzle_offset assert hw_pipette.critical_point() == offsets assert hw_pipette.critical_point(types.CriticalPoint.NOZZLE) == offsets assert hw_pipette.critical_point(types.CriticalPoint.TIP) == offsets diff --git a/api/tests/opentrons/hardware_control/test_pipette_handler.py b/api/tests/opentrons/hardware_control/test_pipette_handler.py index 8da45e68b8f..3bd855024f6 100644 --- a/api/tests/opentrons/hardware_control/test_pipette_handler.py +++ b/api/tests/opentrons/hardware_control/test_pipette_handler.py @@ -16,6 +16,11 @@ TipActionMoveSpec, ) +from opentrons_shared_data.pipette.pipette_definition import ( + PressFitPickUpTipConfiguration, + CamActionPickUpTipConfiguration, +) + @pytest.fixture def mock_pipette(decoy: Decoy) -> Pipette: @@ -106,15 +111,19 @@ def test_plan_check_pick_up_tip_with_presses_argument( decoy.when(mock_pipette.has_tip).then_return(False) decoy.when(mock_pipette.config.quirks).then_return([]) - decoy.when(mock_pipette.pick_up_configurations.distance).then_return(0) - decoy.when(mock_pipette.pick_up_configurations.increment).then_return(0) - decoy.when(mock_pipette.connect_tiprack_distance_mm).then_return(8) - decoy.when(mock_pipette.end_tip_action_retract_distance_mm).then_return(2) - - if presses_input is None: - decoy.when(mock_pipette.pick_up_configurations.presses).then_return( - expected_array_length - ) + decoy.when(mock_pipette.pick_up_configurations.press_fit.presses).then_return( + expected_array_length + ) + decoy.when(mock_pipette.pick_up_configurations.press_fit.distance).then_return(5) + decoy.when(mock_pipette.pick_up_configurations.press_fit.increment).then_return(0) + decoy.when(mock_pipette.pick_up_configurations.press_fit.speed).then_return(10) + decoy.when(mock_pipette.config.end_tip_action_retract_distance_mm).then_return(0) + decoy.when( + mock_pipette.pick_up_configurations.press_fit.current_by_tip_count + ).then_return({1: 1.0}) + decoy.when(mock_pipette.nozzle_manager.current_configuration.tip_count).then_return( + 1 + ) spec, _add_tip_to_instrs = subject.plan_check_pick_up_tip( mount, tip_length, presses, increment @@ -147,30 +156,37 @@ def test_plan_check_pick_up_tip_with_presses_argument_ot3( increment = 1 decoy.when(mock_pipette_ot3.has_tip).then_return(False) - decoy.when(mock_pipette_ot3.pick_up_configurations.presses).then_return(2) - decoy.when(mock_pipette_ot3.pick_up_configurations.increment).then_return(increment) - decoy.when(mock_pipette_ot3.pick_up_configurations.speed).then_return(5.5) - decoy.when(mock_pipette_ot3.pick_up_configurations.distance).then_return(10) - decoy.when(mock_pipette_ot3.pick_up_configurations.current).then_return(1) + decoy.when( + mock_pipette_ot3.get_pick_up_configuration_for_tip_count(channels) + ).then_return( + CamActionPickUpTipConfiguration( + distance=10, + speed=5.5, + prep_move_distance=19.0, + prep_move_speed=10, + currentByTipCount={96: 1.0}, + connectTiprackDistanceMM=8, + ) + if channels == 96 + else PressFitPickUpTipConfiguration( + presses=2, + increment=increment, + distance=10, + speed=5.5, + currentByTipCount={channels: 1.0}, + ) + ) decoy.when(mock_pipette_ot3.plunger_motor_current.run).then_return(1) decoy.when(mock_pipette_ot3.config.quirks).then_return([]) decoy.when(mock_pipette_ot3.channels).then_return(channels) - decoy.when(mock_pipette_ot3.pick_up_configurations.prep_move_distance).then_return( - 19.0 + decoy.when(mock_pipette_ot3.config.end_tip_action_retract_distance_mm).then_return( + 2 ) - decoy.when(mock_pipette_ot3.pick_up_configurations.prep_move_speed).then_return(10) - decoy.when(mock_pipette_ot3.connect_tiprack_distance_mm).then_return(8) - decoy.when(mock_pipette_ot3.end_tip_action_retract_distance_mm).then_return(2) - - if presses_input is None: - decoy.when(mock_pipette_ot3.config.pick_up_presses).then_return( - expected_array_length - ) if channels == 96: - spec = subject_ot3.plan_ht_pick_up_tip() + spec = subject_ot3.plan_ht_pick_up_tip(96) else: - spec = subject_ot3.plan_lt_pick_up_tip(mount, presses, increment) + spec = subject_ot3.plan_lt_pick_up_tip(mount, channels, presses, increment) assert len(spec.tip_action_moves) == expected_array_length assert spec.tip_action_moves == request.getfixturevalue( expected_pick_up_motor_actions diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index b53fa674ef1..333c39e0bfd 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -1,5 +1,5 @@ """Test for the ProtocolEngine-based instrument API core.""" -from typing import cast +from typing import cast, Optional import pytest from decoy import Decoy @@ -20,7 +20,15 @@ ) from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError from opentrons.protocol_engine.clients import SyncClient as EngineClient -from opentrons.protocol_engine.types import FlowRates, TipGeometry +from opentrons.protocol_engine.types import ( + FlowRates, + TipGeometry, + NozzleLayoutConfigurationType, + RowNozzleLayoutConfiguration, + SingleNozzleLayoutConfiguration, + ColumnNozzleLayoutConfiguration, +) +from opentrons.protocol_api._nozzle_layout import NozzleLayout from opentrons.protocol_api.core.engine import InstrumentCore, WellCore, ProtocolCore from opentrons.types import Location, Mount, MountType, Point @@ -874,3 +882,43 @@ def test_has_tip( ).then_return(TipGeometry(length=1, diameter=2, volume=3)) assert subject.has_tip() is True + + +@pytest.mark.parametrize( + argnames=["style", "primary_nozzle", "front_right_nozzle", "expected_model"], + argvalues=[ + [ + NozzleLayout.COLUMN, + "A1", + "H1", + ColumnNozzleLayoutConfiguration(primary_nozzle="A1"), + ], + [ + NozzleLayout.SINGLE, + "H12", + None, + SingleNozzleLayoutConfiguration(primary_nozzle="H12"), + ], + [ + NozzleLayout.ROW, + "A12", + None, + RowNozzleLayoutConfiguration(primary_nozzle="A12"), + ], + ], +) +def test_configure_nozzle_layout( + decoy: Decoy, + mock_engine_client: EngineClient, + subject: InstrumentCore, + style: NozzleLayout, + primary_nozzle: Optional[str], + front_right_nozzle: Optional[str], + expected_model: NozzleLayoutConfigurationType, +) -> None: + """The correct model is passed to the engine client.""" + subject.configure_nozzle_layout(style, primary_nozzle, front_right_nozzle) + + decoy.verify( + mock_engine_client.configure_nozzle_layout(subject._pipette_id, expected_model) + ) diff --git a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py index ee15db98f3b..6cf46c88839 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py @@ -33,6 +33,7 @@ DeckSlotLocation, ModuleLocation, OnLabwareLocation, + AddressableAreaLocation, ModuleDefinition, LabwareMovementStrategy, LoadedLabware, @@ -65,6 +66,7 @@ load_labware_params, ) from opentrons.protocol_api._liquid import Liquid +from opentrons.protocol_api._types import StagingSlotName from opentrons.protocol_api.core.engine.exceptions import InvalidModuleLocationError from opentrons.protocol_api.core.engine.module_core import ( TemperatureModuleCore, @@ -311,6 +313,80 @@ def test_load_labware( assert subject.get_slot_item(DeckSlotName.SLOT_5) is result +def test_load_labware_on_staging_slot( + decoy: Decoy, + mock_engine_client: EngineClient, + subject: ProtocolCore, +) -> None: + """It should issue a LoadLabware command for a labware on a staging slot.""" + decoy.when( + mock_engine_client.state.labware.find_custom_labware_load_params() + ).then_return([EngineLabwareLoadParams("hello", "world", 654)]) + + decoy.when( + load_labware_params.resolve( + "some_labware", + "a_namespace", + 456, + [EngineLabwareLoadParams("hello", "world", 654)], + ) + ).then_return(("some_namespace", 9001)) + + decoy.when( + mock_engine_client.load_labware( + location=AddressableAreaLocation(addressableAreaName="B4"), + load_name="some_labware", + display_name="some_display_name", + namespace="some_namespace", + version=9001, + ) + ).then_return( + commands.LoadLabwareResult( + labwareId="abc123", + definition=LabwareDefinition.construct(), # type: ignore[call-arg] + offsetId=None, + ) + ) + + decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( + LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + ) + + result = subject.load_labware( + load_name="some_labware", + location=StagingSlotName.SLOT_B4, + label="some_display_name", # maps to optional display name + namespace="a_namespace", + version=456, + ) + + assert isinstance(result, LabwareCore) + assert result.labware_id == "abc123" + assert subject.get_labware_cores() == [subject.fixed_trash, result] + + decoy.verify( + deck_conflict.check( + engine_state=mock_engine_client.state, + existing_labware_ids=["fixed-trash-123"], + existing_module_ids=[], + new_labware_id="abc123", + ) + ) + + # TODO(jbl 11-17-2023) this is not hooked up yet to staging slots/addressable areas + # decoy.when( + # mock_engine_client.state.geometry.get_slot_item( + # slot_name=StagingSlotName.SLOT_B4, + # allowed_labware_ids={"fixed-trash-123", "abc123"}, + # allowed_module_ids=set(), + # ) + # ).then_return( + # LoadedLabware.construct(id="abc123") # type: ignore[call-arg] + # ) + # + # assert subject.get_slot_item(StagingSlotName.SLOT_B4) is result + + def test_load_labware_on_labware( decoy: Decoy, mock_engine_client: EngineClient, @@ -511,6 +587,78 @@ def test_load_adapter( assert subject.get_slot_item(DeckSlotName.SLOT_5) is result +def test_load_adapter_on_staging_slot( + decoy: Decoy, + mock_engine_client: EngineClient, + subject: ProtocolCore, +) -> None: + """It should issue a LoadLabware command for an adapter.""" + decoy.when( + mock_engine_client.state.labware.find_custom_labware_load_params() + ).then_return([EngineLabwareLoadParams("hello", "world", 654)]) + + decoy.when( + load_labware_params.resolve( + "some_adapter", + "a_namespace", + 456, + [EngineLabwareLoadParams("hello", "world", 654)], + ) + ).then_return(("some_namespace", 9001)) + + decoy.when( + mock_engine_client.load_labware( + location=AddressableAreaLocation(addressableAreaName="B4"), + load_name="some_adapter", + namespace="some_namespace", + version=9001, + ) + ).then_return( + commands.LoadLabwareResult( + labwareId="abc123", + definition=LabwareDefinition.construct(), # type: ignore[call-arg] + offsetId=None, + ) + ) + + decoy.when(mock_engine_client.state.labware.get_definition("abc123")).then_return( + LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + ) + + result = subject.load_adapter( + load_name="some_adapter", + location=StagingSlotName.SLOT_B4, + namespace="a_namespace", + version=456, + ) + + assert isinstance(result, LabwareCore) + assert result.labware_id == "abc123" + assert subject.get_labware_cores() == [subject.fixed_trash, result] + + decoy.verify( + deck_conflict.check( + engine_state=mock_engine_client.state, + existing_labware_ids=["fixed-trash-123"], + existing_module_ids=[], + new_labware_id="abc123", + ) + ) + + # TODO(jbl 11-17-2023) this is not hooked up yet to staging slots/addressable areas + # decoy.when( + # mock_engine_client.state.geometry.get_slot_item( + # slot_name=StagingSlotName.SLOT_B4, + # allowed_labware_ids={"fixed-trash-123", "abc123"}, + # allowed_module_ids=set(), + # ) + # ).then_return( + # LoadedLabware.construct(id="abc123") # type: ignore[call-arg] + # ) + # + # assert subject.get_slot_item(StagingSlotName.SLOT_B4) is result + + @pytest.mark.parametrize( argnames=["use_gripper", "pause_for_manual_move", "expected_strategy"], argvalues=[ @@ -566,6 +714,38 @@ def test_move_labware( ) +def test_move_labware_on_staging_slot( + decoy: Decoy, + subject: ProtocolCore, + mock_engine_client: EngineClient, + api_version: APIVersion, +) -> None: + """It should issue a move labware command to the engine.""" + decoy.when( + mock_engine_client.state.labware.get_definition("labware-id") + ).then_return( + LabwareDefinition.construct(ordering=[]) # type: ignore[call-arg] + ) + labware = LabwareCore(labware_id="labware-id", engine_client=mock_engine_client) + subject.move_labware( + labware_core=labware, + new_location=StagingSlotName.SLOT_B4, + use_gripper=False, + pause_for_manual_move=True, + pick_up_offset=None, + drop_offset=None, + ) + decoy.verify( + mock_engine_client.move_labware( + labware_id="labware-id", + new_location=AddressableAreaLocation(addressableAreaName="B4"), + strategy=LabwareMovementStrategy.MANUAL_MOVE_WITH_PAUSE, + pick_up_offset=None, + drop_offset=None, + ) + ) + + def test_move_labware_on_non_connected_module( decoy: Decoy, subject: ProtocolCore, @@ -1357,7 +1537,9 @@ def test_get_slot_center( ) -> None: """It should return a slot center from engine state.""" decoy.when( - mock_engine_client.state.labware.get_slot_center_position(DeckSlotName.SLOT_2) + mock_engine_client.state.addressable_areas.get_addressable_area_center( + DeckSlotName.SLOT_2.id + ) ).then_return(Point(1, 2, 3)) result = subject.get_slot_center(DeckSlotName.SLOT_2) diff --git a/api/tests/opentrons/protocol_api/core/legacy/test_protocol_context_implementation.py b/api/tests/opentrons/protocol_api/core/legacy/test_protocol_context_implementation.py index 6961658b712..1af1bb9d691 100644 --- a/api/tests/opentrons/protocol_api/core/legacy/test_protocol_context_implementation.py +++ b/api/tests/opentrons/protocol_api/core/legacy/test_protocol_context_implementation.py @@ -19,6 +19,7 @@ from opentrons.protocols import labware as mock_labware from opentrons.protocols.api_support.util import APIVersionError +from opentrons.protocol_api._types import StagingSlotName from opentrons.protocol_api.core.legacy.module_geometry import ModuleGeometry from opentrons.protocol_api import MAX_SUPPORTED_VERSION, OFF_DECK from opentrons.protocol_api.core.labware import LabwareLoadParams @@ -179,6 +180,20 @@ def test_load_labware_off_deck_raises( ) +def test_load_labware_on_staging_slot_raises( + subject: LegacyProtocolCore, +) -> None: + """It should raise an api error when loading onto a staging slot.""" + with pytest.raises(APIVersionError): + subject.load_labware( + load_name="cool load name", + location=StagingSlotName.SLOT_B4, + label="cool label", + namespace="cool namespace", + version=1337, + ) + + def test_load_labware( decoy: Decoy, mock_deck: Deck, diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 9a38dfef87b..c181add69f5 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -6,6 +6,9 @@ from decoy import Decoy from opentrons.legacy_broker import LegacyBroker +from typing import ContextManager, Optional +from contextlib import nullcontext as does_not_raise + from opentrons.protocols.api_support import instrument as mock_instrument_support from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import ( @@ -26,6 +29,7 @@ from opentrons.protocol_api.core.legacy.legacy_instrument_core import ( LegacyInstrumentCore, ) +from opentrons.protocol_api._nozzle_layout import NozzleLayout from opentrons.types import Location, Mount, Point from opentrons_shared_data.errors.exceptions import ( @@ -934,3 +938,23 @@ def test_prepare_to_aspirate_checks_volume( decoy.when(mock_instrument_core.get_current_volume()).then_return(10) with pytest.raises(CommandPreconditionViolated): subject.prepare_to_aspirate() + + +@pytest.mark.parametrize( + argnames=["style", "primary_nozzle", "front_right_nozzle", "exception"], + argvalues=[ + [NozzleLayout.COLUMN, "A1", "H1", does_not_raise()], + [NozzleLayout.SINGLE, None, None, pytest.raises(ValueError)], + [NozzleLayout.ROW, "E1", None, pytest.raises(ValueError)], + ], +) +def test_configure_nozzle_layout( + subject: InstrumentContext, + style: NozzleLayout, + primary_nozzle: Optional[str], + front_right_nozzle: Optional[str], + exception: ContextManager[None], +) -> None: + """The correct model is passed to the engine client.""" + with exception: + subject.configure_nozzle_layout(style, primary_nozzle, front_right_nozzle) diff --git a/api/tests/opentrons/protocol_api/test_protocol_context.py b/api/tests/opentrons/protocol_api/test_protocol_context.py index d31d0c43ed8..714754b0fa4 100644 --- a/api/tests/opentrons/protocol_api/test_protocol_context.py +++ b/api/tests/opentrons/protocol_api/test_protocol_context.py @@ -28,6 +28,7 @@ validation as mock_validation, Liquid, ) +from opentrons.protocol_api._types import StagingSlotName from opentrons.protocol_api.core.core_map import LoadedCoreMap from opentrons.protocol_api.core.labware import LabwareLoadParams from opentrons.protocol_api.core.common import ( @@ -383,6 +384,52 @@ def test_load_labware_off_deck_raises( ) +def test_load_labware_on_staging_slot( + decoy: Decoy, + mock_core: ProtocolCore, + mock_core_map: LoadedCoreMap, + api_version: APIVersion, + subject: ProtocolContext, +) -> None: + """It should create a labware on a staging slot using its execution core.""" + mock_labware_core = decoy.mock(cls=LabwareCore) + + decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_LABWARE")).then_return( + "lowercase_labware" + ) + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(StagingSlotName.SLOT_B4) + + decoy.when( + mock_core.load_labware( + load_name="lowercase_labware", + location=StagingSlotName.SLOT_B4, + label="some_display_name", + namespace="some_namespace", + version=1337, + ) + ).then_return(mock_labware_core) + + decoy.when(mock_labware_core.get_name()).then_return("Full Name") + decoy.when(mock_labware_core.get_display_name()).then_return("Display Name") + decoy.when(mock_labware_core.get_well_columns()).then_return([]) + + result = subject.load_labware( + load_name="UPPERCASE_LABWARE", + location=42, + label="some_display_name", + namespace="some_namespace", + version=1337, + ) + + assert isinstance(result, Labware) + assert result.name == "Full Name" + + decoy.verify(mock_core_map.add(mock_labware_core, result), times=1) + + def test_load_labware_from_definition( decoy: Decoy, mock_core: ProtocolCore, @@ -468,6 +515,47 @@ def test_load_adapter( decoy.verify(mock_core_map.add(mock_labware_core, result), times=1) +def test_load_adapter_on_staging_slot( + decoy: Decoy, + mock_core: ProtocolCore, + mock_core_map: LoadedCoreMap, + api_version: APIVersion, + subject: ProtocolContext, +) -> None: + """It should create an adapter on a staging slot using its execution core.""" + mock_labware_core = decoy.mock(cls=LabwareCore) + + decoy.when(mock_validation.ensure_lowercase_name("UPPERCASE_ADAPTER")).then_return( + "lowercase_adapter" + ) + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(StagingSlotName.SLOT_B4) + + decoy.when( + mock_core.load_adapter( + load_name="lowercase_adapter", + location=StagingSlotName.SLOT_B4, + namespace="some_namespace", + version=1337, + ) + ).then_return(mock_labware_core) + + decoy.when(mock_labware_core.get_well_columns()).then_return([]) + + result = subject.load_adapter( + load_name="UPPERCASE_ADAPTER", + location=42, + namespace="some_namespace", + version=1337, + ) + + assert isinstance(result, Labware) + + decoy.verify(mock_core_map.add(mock_labware_core, result), times=1) + + def test_load_labware_on_adapter( decoy: Decoy, mock_core: ProtocolCore, @@ -599,6 +687,50 @@ def test_move_labware_to_slot( ) +def test_move_labware_to_staging_slot( + decoy: Decoy, + mock_core: ProtocolCore, + mock_core_map: LoadedCoreMap, + api_version: APIVersion, + subject: ProtocolContext, +) -> None: + """It should move labware to new slot location.""" + drop_offset = {"x": 4, "y": 5, "z": 6} + mock_labware_core = decoy.mock(cls=LabwareCore) + + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(StagingSlotName.SLOT_B4) + decoy.when(mock_labware_core.get_well_columns()).then_return([]) + + movable_labware = Labware( + core=mock_labware_core, + api_version=MAX_SUPPORTED_VERSION, + protocol_core=mock_core, + core_map=mock_core_map, + ) + decoy.when( + mock_validation.ensure_valid_labware_offset_vector(drop_offset) + ).then_return((1, 2, 3)) + subject.move_labware( + labware=movable_labware, + new_location=42, + drop_offset=drop_offset, + ) + + decoy.verify( + mock_core.move_labware( + labware_core=mock_labware_core, + new_location=StagingSlotName.SLOT_B4, + use_gripper=False, + pause_for_manual_move=True, + pick_up_offset=None, + drop_offset=(1, 2, 3), + ) + ) + + def test_move_labware_to_module( decoy: Decoy, mock_core: ProtocolCore, @@ -785,6 +917,26 @@ def test_load_module_with_mag_block_raises(subject: ProtocolContext) -> None: ) +def test_load_module_on_staging_slot_raises( + decoy: Decoy, + mock_core: ProtocolCore, + mock_core_map: LoadedCoreMap, + api_version: APIVersion, + subject: ProtocolContext, +) -> None: + """It should raise when attempting to load a module onto a staging slot.""" + decoy.when(mock_validation.ensure_module_model("spline reticulator")).then_return( + TemperatureModuleModel.TEMPERATURE_V1 + ) + decoy.when(mock_core.robot_type).then_return("OT-3 Standard") + decoy.when( + mock_validation.ensure_and_convert_deck_slot(42, api_version, "OT-3 Standard") + ).then_return(StagingSlotName.SLOT_B4) + + with pytest.raises(ValueError, match="Cannot load a module onto a staging slot."): + subject.load_module(module_name="spline reticulator", location=42) + + def test_loaded_modules( decoy: Decoy, mock_core_map: LoadedCoreMap, diff --git a/api/tests/opentrons/protocol_api/test_validation.py b/api/tests/opentrons/protocol_api/test_validation.py index 13ec1d77db6..ccc418d8159 100644 --- a/api/tests/opentrons/protocol_api/test_validation.py +++ b/api/tests/opentrons/protocol_api/test_validation.py @@ -25,6 +25,7 @@ from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import APIVersionError from opentrons.protocol_api import validation as subject, Well, Labware +from opentrons.protocol_api._types import StagingSlotName @pytest.mark.parametrize( @@ -131,6 +132,11 @@ def test_ensure_pipette_input_invalid(input_value: str) -> None: ("a3", APIVersion(2, 15), "OT-3 Standard", DeckSlotName.SLOT_A3), ("A3", APIVersion(2, 15), "OT-2 Standard", DeckSlotName.FIXED_TRASH), ("A3", APIVersion(2, 15), "OT-3 Standard", DeckSlotName.SLOT_A3), + # Staging slots: + ("A4", APIVersion(2, 16), "OT-3 Standard", StagingSlotName.SLOT_A4), + ("b4", APIVersion(2, 16), "OT-3 Standard", StagingSlotName.SLOT_B4), + ("C4", APIVersion(2, 16), "OT-3 Standard", StagingSlotName.SLOT_C4), + ("d4", APIVersion(2, 16), "OT-3 Standard", StagingSlotName.SLOT_D4), ], ) def test_ensure_and_convert_deck_slot( @@ -162,6 +168,7 @@ def test_ensure_and_convert_deck_slot( APIVersionError, '"A1" requires apiLevel 2.15. Increase your protocol\'s apiLevel, or use slot "10" instead.', ), + ("A4", APIVersion(2, 15), APIVersionError, "Using a staging deck slot"), ], ) @pytest.mark.parametrize("input_robot_type", ["OT-2 Standard", "OT-3 Standard"]) diff --git a/api/tests/opentrons/protocol_engine/commands/conftest.py b/api/tests/opentrons/protocol_engine/commands/conftest.py index f9275b0d1e1..aad3cf21d4a 100644 --- a/api/tests/opentrons/protocol_engine/commands/conftest.py +++ b/api/tests/opentrons/protocol_engine/commands/conftest.py @@ -12,6 +12,7 @@ RailLightsHandler, LabwareMovementHandler, StatusBarHandler, + TipHandler, ) from opentrons.protocol_engine.state import StateView @@ -46,6 +47,12 @@ def pipetting(decoy: Decoy) -> PipettingHandler: return decoy.mock(cls=PipettingHandler) +@pytest.fixture +def tip_handler(decoy: Decoy) -> TipHandler: + """Get a mocked out EquipmentHandler.""" + return decoy.mock(cls=TipHandler) + + @pytest.fixture def run_control(decoy: Decoy) -> RunControlHandler: """Get a mocked out RunControlHandler.""" diff --git a/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py b/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py new file mode 100644 index 00000000000..5635a40897b --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/test_configure_nozzle_layout.py @@ -0,0 +1,379 @@ +"""Test configure nozzle layout commands.""" +import pytest +from decoy import Decoy +from typing import Union, Optional, Dict +from collections import OrderedDict + +from opentrons.protocol_engine.execution import ( + EquipmentHandler, + TipHandler, +) +from opentrons.types import Point +from opentrons.hardware_control.nozzle_manager import NozzleMap + + +from opentrons.protocol_engine.commands.configure_nozzle_layout import ( + ConfigureNozzleLayoutParams, + ConfigureNozzleLayoutResult, + ConfigureNozzleLayoutPrivateResult, + ConfigureNozzleLayoutImplementation, +) + +from opentrons.protocol_engine.types import ( + EmptyNozzleLayoutConfiguration, + ColumnNozzleLayoutConfiguration, + QuadrantNozzleLayoutConfiguration, + SingleNozzleLayoutConfiguration, +) + +NINETY_SIX_ROWS = OrderedDict( + ( + ( + "A", + [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12", + ], + ), + ( + "B", + [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12", + ], + ), + ( + "C", + [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12", + ], + ), + ( + "D", + [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12", + ], + ), + ( + "E", + [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12", + ], + ), + ( + "F", + [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12", + ], + ), + ( + "G", + [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12", + ], + ), + ( + "H", + [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12", + ], + ), + ) +) + + +NINETY_SIX_COLS = OrderedDict( + ( + ("1", ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"]), + ("2", ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"]), + ("3", ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"]), + ("4", ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"]), + ("5", ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"]), + ("6", ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"]), + ("7", ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"]), + ("8", ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"]), + ("9", ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"]), + ("10", ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"]), + ("11", ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"]), + ("12", ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"]), + ) +) + +NINETY_SIX_MAP = OrderedDict( + ( + ("A1", Point(-36.0, -25.5, -259.15)), + ("A2", Point(-27.0, -25.5, -259.15)), + ("A3", Point(-18.0, -25.5, -259.15)), + ("A4", Point(-9.0, -25.5, -259.15)), + ("A5", Point(0.0, -25.5, -259.15)), + ("A6", Point(9.0, -25.5, -259.15)), + ("A7", Point(18.0, -25.5, -259.15)), + ("A8", Point(27.0, -25.5, -259.15)), + ("A9", Point(36.0, -25.5, -259.15)), + ("A10", Point(45.0, -25.5, -259.15)), + ("A11", Point(54.0, -25.5, -259.15)), + ("A12", Point(63.0, -25.5, -259.15)), + ("B1", Point(-36.0, -34.5, -259.15)), + ("B2", Point(-27.0, -34.5, -259.15)), + ("B3", Point(-18.0, -34.5, -259.15)), + ("B4", Point(-9.0, -34.5, -259.15)), + ("B5", Point(0.0, -34.5, -259.15)), + ("B6", Point(9.0, -34.5, -259.15)), + ("B7", Point(18.0, -34.5, -259.15)), + ("B8", Point(27.0, -34.5, -259.15)), + ("B9", Point(36.0, -34.5, -259.15)), + ("B10", Point(45.0, -34.5, -259.15)), + ("B11", Point(54.0, -34.5, -259.15)), + ("B12", Point(63.0, -34.5, -259.15)), + ("C1", Point(-36.0, -43.5, -259.15)), + ("C2", Point(-27.0, -43.5, -259.15)), + ("C3", Point(-18.0, -43.5, -259.15)), + ("C4", Point(-9.0, -43.5, -259.15)), + ("C5", Point(0.0, -43.5, -259.15)), + ("C6", Point(9.0, -43.5, -259.15)), + ("C7", Point(18.0, -43.5, -259.15)), + ("C8", Point(27.0, -43.5, -259.15)), + ("C9", Point(36.0, -43.5, -259.15)), + ("C10", Point(45.0, -43.5, -259.15)), + ("C11", Point(54.0, -43.5, -259.15)), + ("C12", Point(63.0, -43.5, -259.15)), + ("D1", Point(-36.0, -52.5, -259.15)), + ("D2", Point(-27.0, -52.5, -259.15)), + ("D3", Point(-18.0, -52.5, -259.15)), + ("D4", Point(-9.0, -52.5, -259.15)), + ("D5", Point(0.0, -52.5, -259.15)), + ("D6", Point(9.0, -52.5, -259.15)), + ("D7", Point(18.0, -52.5, -259.15)), + ("D8", Point(27.0, -52.5, -259.15)), + ("D9", Point(36.0, -52.5, -259.15)), + ("D10", Point(45.0, -52.5, -259.15)), + ("D11", Point(54.0, -52.5, -259.15)), + ("D12", Point(63.0, -52.5, -259.15)), + ("E1", Point(-36.0, -61.5, -259.15)), + ("E2", Point(-27.0, -61.5, -259.15)), + ("E3", Point(-18.0, -61.5, -259.15)), + ("E4", Point(-9.0, -61.5, -259.15)), + ("E5", Point(0.0, -61.5, -259.15)), + ("E6", Point(9.0, -61.5, -259.15)), + ("E7", Point(18.0, -61.5, -259.15)), + ("E8", Point(27.0, -61.5, -259.15)), + ("E9", Point(36.0, -61.5, -259.15)), + ("E10", Point(45.0, -61.5, -259.15)), + ("E11", Point(54.0, -61.5, -259.15)), + ("E12", Point(63.0, -61.5, -259.15)), + ("F1", Point(-36.0, -70.5, -259.15)), + ("F2", Point(-27.0, -70.5, -259.15)), + ("F3", Point(-18.0, -70.5, -259.15)), + ("F4", Point(-9.0, -70.5, -259.15)), + ("F5", Point(0.0, -70.5, -259.15)), + ("F6", Point(9.0, -70.5, -259.15)), + ("F7", Point(18.0, -70.5, -259.15)), + ("F8", Point(27.0, -70.5, -259.15)), + ("F9", Point(36.0, -70.5, -259.15)), + ("F10", Point(45.0, -70.5, -259.15)), + ("F11", Point(54.0, -70.5, -259.15)), + ("F12", Point(63.0, -70.5, -259.15)), + ("G1", Point(-36.0, -79.5, -259.15)), + ("G2", Point(-27.0, -79.5, -259.15)), + ("G3", Point(-18.0, -79.5, -259.15)), + ("G4", Point(-9.0, -79.5, -259.15)), + ("G5", Point(0.0, -79.5, -259.15)), + ("G6", Point(9.0, -79.5, -259.15)), + ("G7", Point(18.0, -79.5, -259.15)), + ("G8", Point(27.0, -79.5, -259.15)), + ("G9", Point(36.0, -79.5, -259.15)), + ("G10", Point(45.0, -79.5, -259.15)), + ("G11", Point(54.0, -79.5, -259.15)), + ("G12", Point(63.0, -79.5, -259.15)), + ("H1", Point(-36.0, -88.5, -259.15)), + ("H2", Point(-27.0, -88.5, -259.15)), + ("H3", Point(-18.0, -88.5, -259.15)), + ("H4", Point(-9.0, -88.5, -259.15)), + ("H5", Point(0.0, -88.5, -259.15)), + ("H6", Point(9.0, -88.5, -259.15)), + ("H7", Point(18.0, -88.5, -259.15)), + ("H8", Point(27.0, -88.5, -259.15)), + ("H9", Point(36.0, -88.5, -259.15)), + ("H10", Point(45.0, -88.5, -259.15)), + ("H11", Point(54.0, -88.5, -259.15)), + ("H12", Point(63.0, -88.5, -259.15)), + ) +) + + +@pytest.mark.parametrize( + argnames=["request_model", "expected_nozzlemap", "nozzle_params"], + argvalues=[ + [ + SingleNozzleLayoutConfiguration(primary_nozzle="A1"), + NozzleMap.build( + physical_nozzles=OrderedDict({"A1": Point(0, 0, 0)}), + physical_rows=OrderedDict({"A": ["A1"]}), + physical_columns=OrderedDict({"1": ["A1"]}), + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="A1", + ), + {"primary_nozzle": "A1"}, + ], + [ + ColumnNozzleLayoutConfiguration(primary_nozzle="A1"), + NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="H1", + ), + {"primary_nozzle": "A1", "front_right_nozzle": "H1"}, + ], + [ + QuadrantNozzleLayoutConfiguration( + primary_nozzle="A1", front_right_nozzle="E1" + ), + NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="E1", + ), + {"primary_nozzle": "A1", "front_right_nozzle": "E1"}, + ], + [ + EmptyNozzleLayoutConfiguration(), + None, + {}, + ], + ], +) +async def test_configure_nozzle_layout_implementation( + decoy: Decoy, + equipment: EquipmentHandler, + tip_handler: TipHandler, + request_model: Union[ + EmptyNozzleLayoutConfiguration, + ColumnNozzleLayoutConfiguration, + QuadrantNozzleLayoutConfiguration, + SingleNozzleLayoutConfiguration, + ], + expected_nozzlemap: Optional[NozzleMap], + nozzle_params: Dict[str, str], +) -> None: + """A ConfigureForVolume command should have an execution implementation.""" + subject = ConfigureNozzleLayoutImplementation( + equipment=equipment, tip_handler=tip_handler + ) + + requested_nozzle_layout = ConfigureNozzleLayoutParams( + pipetteId="pipette-id", + configuration_params=request_model, + ) + + decoy.when( + await tip_handler.available_for_nozzle_layout( + "pipette-id", **request_model.dict() + ) + ).then_return(nozzle_params) + + decoy.when( + await equipment.configure_nozzle_layout( + pipette_id="pipette-id", + **nozzle_params, + ) + ).then_return(expected_nozzlemap) + + result, private_result = await subject.execute(requested_nozzle_layout) + + assert result == ConfigureNozzleLayoutResult() + assert private_result == ConfigureNozzleLayoutPrivateResult( + pipette_id="pipette-id", nozzle_map=expected_nozzlemap + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py index 7551c67ea25..4a3c547c07a 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py @@ -86,11 +86,16 @@ async def test_drop_tip_implementation( homeAfter=True, ) + decoy.when( + mock_state_view.pipettes.get_is_partially_configured(pipette_id="abc") + ).then_return(False) + decoy.when( mock_state_view.geometry.get_checked_tip_drop_location( pipette_id="abc", labware_id="123", well_location=DropTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), + partially_configured=False, ) ).then_return(WellLocation(offset=WellOffset(x=4, y=5, z=6))) @@ -142,9 +147,16 @@ async def test_drop_tip_with_alternating_locations( ) ).then_return(drop_location) + decoy.when( + mock_state_view.pipettes.get_is_partially_configured(pipette_id="abc") + ).then_return(False) + decoy.when( mock_state_view.geometry.get_checked_tip_drop_location( - pipette_id="abc", labware_id="123", well_location=drop_location + pipette_id="abc", + labware_id="123", + well_location=drop_location, + partially_configured=False, ) ).then_return(WellLocation(offset=WellOffset(x=4, y=5, z=6))) diff --git a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py index c8ce23e4756..d9052872cff 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py @@ -2,6 +2,9 @@ import pytest from decoy import Decoy +from typing import Dict, ContextManager, Optional +from contextlib import nullcontext as does_not_raise + from opentrons.types import Mount, MountType from opentrons.hardware_control import API as HardwareAPI @@ -9,7 +12,10 @@ from opentrons.protocol_engine.state import StateView from opentrons.protocol_engine.types import TipGeometry from opentrons.protocol_engine.resources import LabwareDataProvider - +from opentrons_shared_data.errors.exceptions import ( + CommandPreconditionViolated, + CommandParameterLimitViolated, +) from opentrons.protocol_engine.execution.tip_handler import ( HardwareTipHandler, VirtualTipHandler, @@ -191,6 +197,94 @@ async def test_add_tip( ) +@pytest.mark.parametrize( + argnames=[ + "test_channels", + "style", + "primary_nozzle", + "front_nozzle", + "exception", + "expected_result", + "tip_result", + ], + argvalues=[ + [ + 8, + "COLUMN", + "A1", + None, + does_not_raise(), + {"primary_nozzle": "A1", "front_right_nozzle": "H1"}, + None, + ], + [ + 8, + "ROW", + "A1", + None, + pytest.raises(CommandParameterLimitViolated), + None, + None, + ], + [8, "SINGLE", "A1", None, does_not_raise(), {"primary_nozzle": "A1"}, None], + [ + 1, + "SINGLE", + "A1", + None, + pytest.raises(CommandPreconditionViolated), + None, + None, + ], + [ + 8, + "COLUMN", + "A1", + None, + pytest.raises(CommandPreconditionViolated), + None, + TipGeometry(length=50, diameter=5, volume=300), + ], + ], +) +async def test_available_nozzle_layout( + decoy: Decoy, + mock_state_view: StateView, + mock_hardware_api: HardwareAPI, + mock_labware_data_provider: LabwareDataProvider, + test_channels: int, + style: str, + primary_nozzle: Optional[str], + front_nozzle: Optional[str], + exception: ContextManager[None], + expected_result: Optional[Dict[str, str]], + tip_result: Optional[TipGeometry], +) -> None: + """The virtual and hardware pipettes should return the same data and error at the same time.""" + hw_subject = HardwareTipHandler( + state_view=mock_state_view, + hardware_api=mock_hardware_api, + labware_data_provider=mock_labware_data_provider, + ) + virtual_subject = VirtualTipHandler(state_view=mock_state_view) + decoy.when(mock_state_view.pipettes.get_channels("pipette-id")).then_return( + test_channels + ) + decoy.when(mock_state_view.pipettes.get_attached_tip("pipette-id")).then_return( + tip_result + ) + + with exception: + hw_result = await hw_subject.available_for_nozzle_layout( + "pipette-id", style, primary_nozzle, front_nozzle + ) + virtual_result = await virtual_subject.available_for_nozzle_layout( + "pipette-id", style, primary_nozzle, front_nozzle + ) + + assert hw_result == virtual_result == expected_result + + async def test_virtual_pick_up_tip( decoy: Decoy, mock_state_view: StateView, diff --git a/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py new file mode 100644 index 00000000000..b8528fcaa7c --- /dev/null +++ b/api/tests/opentrons/protocol_engine/resources/test_deck_configuration_provider.py @@ -0,0 +1,339 @@ +"""Test deck configuration provider.""" +from typing import List, Set + +import pytest +from pytest_lazyfixture import lazy_fixture # type: ignore[import] + +from opentrons_shared_data.deck import load as load_deck +from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 + +from opentrons.types import Point + +from opentrons.protocol_engine.errors import ( + FixtureDoesNotExistError, + CutoutDoesNotExistError, + AddressableAreaDoesNotExistError, + FixtureDoesNotProvideAreasError, +) +from opentrons.protocol_engine.types import ( + AddressableArea, + PotentialCutoutFixture, + DeckPoint, + Dimensions, + AddressableOffsetVector, +) +from opentrons.protocols.api_support.deck_type import ( + SHORT_TRASH_DECK, + STANDARD_OT2_DECK, + STANDARD_OT3_DECK, +) + +from opentrons.protocol_engine.resources import deck_configuration_provider as subject + + +@pytest.fixture(scope="session") +def ot2_standard_deck_def() -> DeckDefinitionV4: + """Get the OT-2 standard deck definition.""" + return load_deck(STANDARD_OT2_DECK, 4) + + +@pytest.fixture(scope="session") +def ot2_short_trash_deck_def() -> DeckDefinitionV4: + """Get the OT-2 standard deck definition.""" + return load_deck(SHORT_TRASH_DECK, 4) + + +@pytest.fixture(scope="session") +def ot3_standard_deck_def() -> DeckDefinitionV4: + """Get the OT-2 standard deck definition.""" + return load_deck(STANDARD_OT3_DECK, 4) + + +@pytest.mark.parametrize( + ("cutout_id", "expected_deck_point", "deck_def"), + [ + ( + "cutout5", + DeckPoint(x=132.5, y=90.5, z=0.0), + lazy_fixture("ot2_standard_deck_def"), + ), + ( + "cutout5", + DeckPoint(x=132.5, y=90.5, z=0.0), + lazy_fixture("ot2_short_trash_deck_def"), + ), + ( + "cutoutC2", + DeckPoint(x=164.0, y=107, z=0.0), + lazy_fixture("ot3_standard_deck_def"), + ), + ], +) +def test_get_cutout_position( + cutout_id: str, + expected_deck_point: DeckPoint, + deck_def: DeckDefinitionV4, +) -> None: + """It should get the deck position for the requested cutout id.""" + cutout_position = subject.get_cutout_position(cutout_id, deck_def) + assert cutout_position == expected_deck_point + + +def test_get_cutout_position_raises( + ot3_standard_deck_def: DeckDefinitionV4, +) -> None: + """It should raise if there is no cutout with that ID in the deck definition.""" + with pytest.raises(CutoutDoesNotExistError): + subject.get_cutout_position("theFunCutout", ot3_standard_deck_def) + + +@pytest.mark.parametrize( + ("cutout_fixture_id", "expected_display_name", "deck_def"), + [ + ("singleStandardSlot", "Standard Slot", lazy_fixture("ot2_standard_deck_def")), + ( + "singleStandardSlot", + "Standard Slot", + lazy_fixture("ot2_short_trash_deck_def"), + ), + ( + "singleRightSlot", + "Standard Slot Right", + lazy_fixture("ot3_standard_deck_def"), + ), + ], +) +def test_get_cutout_fixture( + cutout_fixture_id: str, + expected_display_name: str, + deck_def: DeckDefinitionV4, +) -> None: + """It should get the cutout fixture given the cutout fixture id.""" + cutout_fixture = subject.get_cutout_fixture(cutout_fixture_id, deck_def) + assert cutout_fixture["displayName"] == expected_display_name + + +def test_get_cutout_fixture_raises( + ot3_standard_deck_def: DeckDefinitionV4, +) -> None: + """It should raise if the given cutout fixture id does not exist.""" + with pytest.raises(FixtureDoesNotExistError): + subject.get_cutout_fixture("theFunFixture", ot3_standard_deck_def) + + +@pytest.mark.parametrize( + ("cutout_fixture_id", "cutout_id", "expected_areas", "deck_def"), + [ + ( + "singleStandardSlot", + "cutout1", + ["1"], + lazy_fixture("ot2_standard_deck_def"), + ), + ( + "singleStandardSlot", + "cutout1", + ["1"], + lazy_fixture("ot2_short_trash_deck_def"), + ), + ( + "stagingAreaRightSlot", + "cutoutD3", + ["D3", "D4"], + lazy_fixture("ot3_standard_deck_def"), + ), + ], +) +def test_get_provided_addressable_area_names( + cutout_fixture_id: str, + cutout_id: str, + expected_areas: List[str], + deck_def: DeckDefinitionV4, +) -> None: + """It should get the provided addressable area for the cutout fixture and cutout.""" + provided_addressable_areas = subject.get_provided_addressable_area_names( + cutout_fixture_id, cutout_id, deck_def + ) + assert provided_addressable_areas == expected_areas + + +def test_get_provided_addressable_area_raises( + ot3_standard_deck_def: DeckDefinitionV4, +) -> None: + """It should raise if the cutout fixture does not provide areas for the given cutout id.""" + with pytest.raises(FixtureDoesNotProvideAreasError): + subject.get_provided_addressable_area_names( + "singleRightSlot", "theFunCutout", ot3_standard_deck_def + ) + + +@pytest.mark.parametrize( + ( + "addressable_area_name", + "expected_cutout_id", + "expected_potential_fixtures", + "deck_def", + ), + [ + ( + "3", + "cutout3", + { + PotentialCutoutFixture( + cutout_id="cutout3", + cutout_fixture_id="singleStandardSlot", + ) + }, + lazy_fixture("ot2_standard_deck_def"), + ), + ( + "3", + "cutout3", + { + PotentialCutoutFixture( + cutout_id="cutout3", + cutout_fixture_id="singleStandardSlot", + ) + }, + lazy_fixture("ot2_short_trash_deck_def"), + ), + ( + "D3", + "cutoutD3", + { + PotentialCutoutFixture( + cutout_id="cutoutD3", cutout_fixture_id="singleRightSlot" + ), + PotentialCutoutFixture( + cutout_id="cutoutD3", cutout_fixture_id="stagingAreaRightSlot" + ), + }, + lazy_fixture("ot3_standard_deck_def"), + ), + ], +) +def test_get_potential_cutout_fixtures( + addressable_area_name: str, + expected_cutout_id: str, + expected_potential_fixtures: Set[PotentialCutoutFixture], + deck_def: DeckDefinitionV4, +) -> None: + """It should get a cutout id and a set of potential cutout fixtures for an addressable area name.""" + cutout_id, potential_fixtures = subject.get_potential_cutout_fixtures( + addressable_area_name, deck_def + ) + assert cutout_id == expected_cutout_id + assert potential_fixtures == expected_potential_fixtures + + +def test_get_potential_cutout_fixtures_raises( + ot3_standard_deck_def: DeckDefinitionV4, +) -> None: + """It should raise if there is no fixtures that provide the requested area.""" + with pytest.raises(AssertionError): + subject.get_potential_cutout_fixtures("theFunArea", ot3_standard_deck_def) + + +# TODO put in fixed trash for OT2 decks +@pytest.mark.parametrize( + ("addressable_area_name", "expected_addressable_area", "deck_def"), + [ + ( + "1", + AddressableArea( + area_name="1", + display_name="Slot 1", + bounding_box=Dimensions(x=128.0, y=86.0, z=0), + position=AddressableOffsetVector(x=1, y=2, z=3), + compatible_module_types=[ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType", + ], + drop_tip_location=None, + drop_labware_location=None, + ), + lazy_fixture("ot2_standard_deck_def"), + ), + ( + "1", + AddressableArea( + area_name="1", + display_name="Slot 1", + bounding_box=Dimensions(x=128.0, y=86.0, z=0), + position=AddressableOffsetVector(x=1, y=2, z=3), + compatible_module_types=[ + "magneticModuleType", + "temperatureModuleType", + "heaterShakerModuleType", + ], + drop_tip_location=None, + drop_labware_location=None, + ), + lazy_fixture("ot2_short_trash_deck_def"), + ), + ( + "D1", + AddressableArea( + area_name="D1", + display_name="Slot D1", + bounding_box=Dimensions(x=128.0, y=86.0, z=0), + position=AddressableOffsetVector(x=1, y=2, z=3), + compatible_module_types=[ + "temperatureModuleType", + "heaterShakerModuleType", + "magneticBlockType", + ], + drop_tip_location=None, + drop_labware_location=None, + ), + lazy_fixture("ot3_standard_deck_def"), + ), + ( + "movableTrashB3", + AddressableArea( + area_name="movableTrashB3", + display_name="Trash Bin", + bounding_box=Dimensions(x=246.5, y=91.5, z=40), + position=AddressableOffsetVector(x=-16, y=-0.75, z=3), + compatible_module_types=[], + drop_tip_location=Point(x=124.25, y=47.75, z=43.0), + drop_labware_location=None, + ), + lazy_fixture("ot3_standard_deck_def"), + ), + ( + "gripperWasteChute", + AddressableArea( + area_name="gripperWasteChute", + display_name="Gripper Waste Chute", + bounding_box=Dimensions(x=155.0, y=86.0, z=154.0), + position=AddressableOffsetVector(x=-12.5, y=2, z=3), + compatible_module_types=[], + drop_tip_location=None, + drop_labware_location=Point(x=65, y=31, z=139.5), + ), + lazy_fixture("ot3_standard_deck_def"), + ), + ], +) +def test_get_addressable_area_from_name( + addressable_area_name: str, + expected_addressable_area: AddressableArea, + deck_def: DeckDefinitionV4, +) -> None: + """It should get the deck position for the requested cutout id.""" + addressable_area = subject.get_addressable_area_from_name( + addressable_area_name, DeckPoint(x=1, y=2, z=3), deck_def + ) + assert addressable_area == expected_addressable_area + + +def test_get_addressable_area_from_name_raises( + ot3_standard_deck_def: DeckDefinitionV4, +) -> None: + """It should raise if there is no addressable area by that name in the deck.""" + with pytest.raises(AddressableAreaDoesNotExistError): + subject.get_addressable_area_from_name( + "theFunArea", DeckPoint(x=1, y=2, z=3), ot3_standard_deck_def + ) diff --git a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py index 1eb3787a509..444ced17857 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py @@ -125,6 +125,44 @@ def test_load_virtual_pipette_by_model_string( ) +def test_load_virtual_pipette_nozzle_layout( + subject_instance: VirtualPipetteDataProvider, +) -> None: + """It should return a NozzleMap object.""" + subject_instance.configure_virtual_pipette_nozzle_layout( + "my-pipette", "p300_multi_v2.1", "A1", "E1", "A1" + ) + result = subject_instance.get_nozzle_layout_for_pipette("my-pipette") + assert result.configuration.value == "COLUMN" + assert result.starting_nozzle == "A1" + assert result.front_right == "E1" + assert result.back_left == "A1" + + subject_instance.configure_virtual_pipette_nozzle_layout( + "my-pipette", "p300_multi_v2.1" + ) + result = subject_instance.get_nozzle_layout_for_pipette("my-pipette") + assert result.configuration.value == "FULL" + + subject_instance.configure_virtual_pipette_nozzle_layout( + "my-96-pipette", "p1000_96_v3.5", "A1", "A12", "A1" + ) + result = subject_instance.get_nozzle_layout_for_pipette("my-96-pipette") + assert result.configuration.value == "ROW" + + subject_instance.configure_virtual_pipette_nozzle_layout( + "my-96-pipette", "p1000_96_v3.5", "A1", "A1" + ) + result = subject_instance.get_nozzle_layout_for_pipette("my-96-pipette") + assert result.configuration.value == "SINGLE" + + subject_instance.configure_virtual_pipette_nozzle_layout( + "my-96-pipette", "p1000_96_v3.5", "A1", "H1" + ) + result = subject_instance.get_nozzle_layout_for_pipette("my-96-pipette") + assert result.configuration.value == "COLUMN" + + def test_get_pipette_static_config( supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: @@ -164,6 +202,7 @@ def test_get_pipette_static_config( "default_aspirate_speeds": {"2.0": 5.021202, "2.6": 10.042404}, "default_push_out_volume": 3, "supported_tips": {pip_types.PipetteTipType.t300: supported_tip_fixture}, + "current_nozzle_map": None, } result = subject.get_pipette_static_config(pipette_dict) diff --git a/api/tests/opentrons/protocol_engine/state/command_fixtures.py b/api/tests/opentrons/protocol_engine/state/command_fixtures.py index ef548377a3e..fc576d5c683 100644 --- a/api/tests/opentrons/protocol_engine/state/command_fixtures.py +++ b/api/tests/opentrons/protocol_engine/state/command_fixtures.py @@ -9,9 +9,12 @@ from opentrons.protocol_engine import ErrorOccurrence, commands as cmd from opentrons.protocol_engine.types import ( DeckPoint, + ModuleModel, + ModuleDefinition, MovementAxis, WellLocation, LabwareLocation, + DeckSlotLocation, LabwareMovementStrategy, ) @@ -159,6 +162,30 @@ def create_load_pipette_command( ) +def create_load_module_command( + module_id: str, + location: DeckSlotLocation, + model: ModuleModel, +) -> cmd.LoadModule: + """Get a completed LoadModule command.""" + params = cmd.LoadModuleParams(moduleId=module_id, location=location, model=model) + result = cmd.LoadModuleResult( + moduleId=module_id, + model=model, + serialNumber=None, + definition=ModuleDefinition.construct(), # type: ignore[call-arg] + ) + + return cmd.LoadModule( + id="command-id", + key="command-key", + status=cmd.CommandStatus.SUCCEEDED, + createdAt=datetime.now(), + params=params, + result=result, + ) + + def create_aspirate_command( pipette_id: str, volume: float, diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py new file mode 100644 index 00000000000..018332293b4 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_store.py @@ -0,0 +1,319 @@ +"""Addressable area state store tests.""" +import pytest + +from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons_shared_data.labware.labware_definition import Parameters +from opentrons.protocols.models import LabwareDefinition +from opentrons.types import DeckSlotName + +from opentrons.protocol_engine.commands import Command +from opentrons.protocol_engine.actions import UpdateCommandAction +from opentrons.protocol_engine.errors import ( + # AreaNotInDeckConfigurationError, + IncompatibleAddressableAreaError, +) +from opentrons.protocol_engine.state import Config +from opentrons.protocol_engine.state.addressable_areas import ( + AddressableAreaStore, + AddressableAreaState, + DeckConfiguration, +) +from opentrons.protocol_engine.types import ( + DeckType, + ModuleModel, + LabwareMovementStrategy, + DeckSlotLocation, + AddressableAreaLocation, +) + +from .command_fixtures import ( + create_load_labware_command, + create_load_module_command, + create_move_labware_command, +) + + +def _make_deck_config() -> DeckConfiguration: + return [ + ("cutoutA1", "singleLeftSlot"), + ("cutoutB1", "singleLeftSlot"), + ("cutoutC1", "singleLeftSlot"), + ("cutoutD1", "singleLeftSlot"), + ("cutoutA2", "singleCenterSlot"), + ("cutoutB2", "singleCenterSlot"), + ("cutoutC2", "singleCenterSlot"), + ("cutoutD2", "singleCenterSlot"), + ("cutoutA3", "trashBinAdapter"), + ("cutoutB3", "singleRightSlot"), + ("cutoutC3", "stagingAreaRightSlot"), + ("cutoutD3", "wasteChuteRightAdapterNoCover"), + ] + + +@pytest.fixture +def simulated_subject( + ot3_standard_deck_def: DeckDefinitionV4, +) -> AddressableAreaStore: + """Get an AddressableAreaStore test subject, under simulated deck conditions.""" + return AddressableAreaStore( + deck_configuration=[], + config=Config( + use_simulated_deck_config=True, + robot_type="OT-3 Standard", + deck_type=DeckType.OT3_STANDARD, + ), + deck_definition=ot3_standard_deck_def, + ) + + +@pytest.fixture +def subject( + ot3_standard_deck_def: DeckDefinitionV4, +) -> AddressableAreaStore: + """Get an AddressableAreaStore test subject.""" + return AddressableAreaStore( + deck_configuration=_make_deck_config(), + config=Config( + use_simulated_deck_config=False, + robot_type="OT-3 Standard", + deck_type=DeckType.OT3_STANDARD, + ), + deck_definition=ot3_standard_deck_def, + ) + + +def test_initial_state_simulated( + ot3_standard_deck_def: DeckDefinitionV4, + simulated_subject: AddressableAreaStore, +) -> None: + """It should create the Addressable Area store with no loaded addressable areas.""" + assert simulated_subject.state == AddressableAreaState( + loaded_addressable_areas_by_name={}, + potential_cutout_fixtures_by_cutout_id={}, + deck_definition=ot3_standard_deck_def, + use_simulated_deck_config=True, + ) + + +def test_initial_state( + ot3_standard_deck_def: DeckDefinitionV4, + subject: AddressableAreaStore, +) -> None: + """It should create the Addressable Area store with loaded addressable areas.""" + assert subject.state.potential_cutout_fixtures_by_cutout_id == {} + assert not subject.state.use_simulated_deck_config + assert subject.state.deck_definition == ot3_standard_deck_def + # Loading 9 regular slots, 1 trash, 2 Staging Area slots and 3 waste chute types + assert len(subject.state.loaded_addressable_areas_by_name) == 15 + + +@pytest.mark.parametrize( + ("command", "expected_area"), + ( + ( + create_load_labware_command( + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + labware_id="test-labware-id", + definition=LabwareDefinition.construct( # type: ignore[call-arg] + parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] + namespace="bleh", + version=123, + ), + offset_id="offset-id", + display_name="display-name", + ), + "A1", + ), + ( + create_load_labware_command( + location=AddressableAreaLocation(addressableAreaName="A4"), + labware_id="test-labware-id", + definition=LabwareDefinition.construct( # type: ignore[call-arg] + parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] + namespace="bleh", + version=123, + ), + offset_id="offset-id", + display_name="display-name", + ), + "A4", + ), + ( + create_load_module_command( + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + module_id="test-module-id", + model=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "A1", + ), + ( + create_move_labware_command( + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + strategy=LabwareMovementStrategy.USING_GRIPPER, + ), + "A1", + ), + ( + create_move_labware_command( + new_location=AddressableAreaLocation(addressableAreaName="A4"), + strategy=LabwareMovementStrategy.USING_GRIPPER, + ), + "A4", + ), + ), +) +def test_addressable_area_referencing_commands_load_on_simulated_deck( + command: Command, + expected_area: str, + simulated_subject: AddressableAreaStore, +) -> None: + """It should check and store the addressable area when referenced in a command.""" + simulated_subject.handle_action( + UpdateCommandAction(private_result=None, command=command) + ) + assert expected_area in simulated_subject.state.loaded_addressable_areas_by_name + + +@pytest.mark.parametrize( + "command", + ( + create_load_labware_command( + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), + labware_id="test-labware-id", + definition=LabwareDefinition.construct( # type: ignore[call-arg] + parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] + namespace="bleh", + version=123, + ), + offset_id="offset-id", + display_name="display-name", + ), + create_load_module_command( + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), + module_id="test-module-id", + model=ModuleModel.TEMPERATURE_MODULE_V2, + ), + create_move_labware_command( + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), + strategy=LabwareMovementStrategy.USING_GRIPPER, + ), + ), +) +def test_handles_command_simulated_raises( + command: Command, + simulated_subject: AddressableAreaStore, +) -> None: + """It should raise when two incompatible areas are referenced.""" + initial_command = create_move_labware_command( + new_location=AddressableAreaLocation(addressableAreaName="gripperWasteChute"), + strategy=LabwareMovementStrategy.USING_GRIPPER, + ) + + simulated_subject.handle_action( + UpdateCommandAction(private_result=None, command=initial_command) + ) + + with pytest.raises(IncompatibleAddressableAreaError): + simulated_subject.handle_action( + UpdateCommandAction(private_result=None, command=command) + ) + + +@pytest.mark.parametrize( + ("command", "expected_area"), + ( + ( + create_load_labware_command( + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + labware_id="test-labware-id", + definition=LabwareDefinition.construct( # type: ignore[call-arg] + parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] + namespace="bleh", + version=123, + ), + offset_id="offset-id", + display_name="display-name", + ), + "A1", + ), + ( + create_load_labware_command( + location=AddressableAreaLocation(addressableAreaName="C4"), + labware_id="test-labware-id", + definition=LabwareDefinition.construct( # type: ignore[call-arg] + parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] + namespace="bleh", + version=123, + ), + offset_id="offset-id", + display_name="display-name", + ), + "C4", + ), + ( + create_load_module_command( + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + module_id="test-module-id", + model=ModuleModel.TEMPERATURE_MODULE_V2, + ), + "A1", + ), + ( + create_move_labware_command( + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_A1), + strategy=LabwareMovementStrategy.USING_GRIPPER, + ), + "A1", + ), + ( + create_move_labware_command( + new_location=AddressableAreaLocation(addressableAreaName="C4"), + strategy=LabwareMovementStrategy.USING_GRIPPER, + ), + "C4", + ), + ), +) +def test_addressable_area_referencing_commands_load( + command: Command, + expected_area: str, + subject: AddressableAreaStore, +) -> None: + """It should check that the addressable area is in the deck config.""" + subject.handle_action(UpdateCommandAction(private_result=None, command=command)) + assert expected_area in subject.state.loaded_addressable_areas_by_name + + +# TODO Uncomment this out once this check is back in +# @pytest.mark.parametrize( +# "command", +# ( +# create_load_labware_command( +# location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), +# labware_id="test-labware-id", +# definition=LabwareDefinition.construct( # type: ignore[call-arg] +# parameters=Parameters.construct(loadName="blah"), # type: ignore[call-arg] +# namespace="bleh", +# version=123, +# ), +# offset_id="offset-id", +# display_name="display-name", +# ), +# create_load_module_command( +# location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), +# module_id="test-module-id", +# model=ModuleModel.TEMPERATURE_MODULE_V2, +# ), +# create_move_labware_command( +# new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_D3), +# strategy=LabwareMovementStrategy.USING_GRIPPER, +# ), +# ), +# ) +# def test_handles_load_labware_raises( +# command: Command, +# subject: AddressableAreaStore, +# ) -> None: +# """It should raise when referencing an addressable area not in the deck config.""" +# with pytest.raises(AreaNotInDeckConfigurationError): +# subject.handle_action(UpdateCommandAction(private_result=None, command=command)) diff --git a/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py new file mode 100644 index 00000000000..fc084d13f09 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/state/test_addressable_area_view.py @@ -0,0 +1,250 @@ +"""Addressable area state view tests.""" +import inspect + +import pytest +from decoy import Decoy +from typing import Dict, Set, Optional, cast + +from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 +from opentrons.types import Point, DeckSlotName + +from opentrons.protocol_engine.errors import ( + AreaNotInDeckConfigurationError, + IncompatibleAddressableAreaError, + # SlotDoesNotExistError, +) +from opentrons.protocol_engine.resources import deck_configuration_provider +from opentrons.protocol_engine.state.addressable_areas import ( + AddressableAreaState, + AddressableAreaView, +) +from opentrons.protocol_engine.types import ( + AddressableArea, + PotentialCutoutFixture, + Dimensions, + DeckPoint, + AddressableOffsetVector, +) + + +@pytest.fixture(autouse=True) +def patch_mock_move_types(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None: + """Mock out move_types.py functions.""" + for name, func in inspect.getmembers( + deck_configuration_provider, inspect.isfunction + ): + monkeypatch.setattr(deck_configuration_provider, name, decoy.mock(func=func)) + + +def get_addressable_area_view( + loaded_addressable_areas_by_name: Optional[Dict[str, AddressableArea]] = None, + potential_cutout_fixtures_by_cutout_id: Optional[ + Dict[str, Set[PotentialCutoutFixture]] + ] = None, + deck_definition: Optional[DeckDefinitionV4] = None, + use_simulated_deck_config: bool = False, +) -> AddressableAreaView: + """Get a labware view test subject.""" + state = AddressableAreaState( + loaded_addressable_areas_by_name=loaded_addressable_areas_by_name or {}, + potential_cutout_fixtures_by_cutout_id=potential_cutout_fixtures_by_cutout_id + or {}, + deck_definition=deck_definition or cast(DeckDefinitionV4, {"otId": "fake"}), + use_simulated_deck_config=use_simulated_deck_config, + ) + + return AddressableAreaView(state=state) + + +def test_get_loaded_addressable_area() -> None: + """It should get the loaded addressable area.""" + addressable_area = AddressableArea( + area_name="area", + display_name="fancy name", + bounding_box=Dimensions(x=1, y=2, z=3), + position=AddressableOffsetVector(x=7, y=8, z=9), + compatible_module_types=["magneticModuleType"], + drop_tip_location=Point(11, 22, 33), + drop_labware_location=None, + ) + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={"abc": addressable_area} + ) + + assert subject.get_addressable_area("abc") is addressable_area + + +def test_get_loaded_addressable_area_raises() -> None: + """It should raise if the addressable area does not exist.""" + subject = get_addressable_area_view() + + with pytest.raises(AreaNotInDeckConfigurationError): + subject.get_addressable_area("abc") + + +def test_get_addressable_area_for_simulation_already_loaded() -> None: + """It should get the addressable area for a simulation that has not been loaded yet.""" + addressable_area = AddressableArea( + area_name="area", + display_name="fancy name", + bounding_box=Dimensions(x=1, y=2, z=3), + position=AddressableOffsetVector(x=7, y=8, z=9), + compatible_module_types=["magneticModuleType"], + drop_tip_location=Point(11, 22, 33), + drop_labware_location=None, + ) + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={"abc": addressable_area}, + use_simulated_deck_config=True, + ) + + assert subject.get_addressable_area("abc") is addressable_area + + +def test_get_addressable_area_for_simulation_not_loaded(decoy: Decoy) -> None: + """It should get the addressable area for a simulation that has not been loaded yet.""" + subject = get_addressable_area_view( + potential_cutout_fixtures_by_cutout_id={ + "123": {PotentialCutoutFixture(cutout_id="123", cutout_fixture_id="blah")} + }, + use_simulated_deck_config=True, + ) + + addressable_area = AddressableArea( + area_name="area", + display_name="fancy name", + bounding_box=Dimensions(x=1, y=2, z=3), + position=AddressableOffsetVector(x=7, y=8, z=9), + compatible_module_types=["magneticModuleType"], + drop_tip_location=Point(11, 22, 33), + drop_labware_location=None, + ) + + decoy.when( + deck_configuration_provider.get_potential_cutout_fixtures( + "abc", subject.state.deck_definition + ) + ).then_return( + ("123", {PotentialCutoutFixture(cutout_id="123", cutout_fixture_id="blah")}) + ) + + decoy.when( + deck_configuration_provider.get_cutout_position( + "123", subject.state.deck_definition + ) + ).then_return(DeckPoint(x=1, y=2, z=3)) + + decoy.when( + deck_configuration_provider.get_addressable_area_from_name( + "abc", DeckPoint(x=1, y=2, z=3), subject.state.deck_definition + ) + ).then_return(addressable_area) + + assert subject.get_addressable_area("abc") is addressable_area + + +def test_get_addressable_area_for_simulation_raises(decoy: Decoy) -> None: + """It should raise if the requested addressable area is incompatible with loaded ones.""" + subject = get_addressable_area_view( + potential_cutout_fixtures_by_cutout_id={ + "123": {PotentialCutoutFixture(cutout_id="789", cutout_fixture_id="bleh")} + }, + use_simulated_deck_config=True, + ) + + decoy.when( + deck_configuration_provider.get_potential_cutout_fixtures( + "abc", subject.state.deck_definition + ) + ).then_return( + ("123", {PotentialCutoutFixture(cutout_id="123", cutout_fixture_id="blah")}) + ) + + decoy.when( + deck_configuration_provider.get_provided_addressable_area_names( + "bleh", "789", subject.state.deck_definition + ) + ).then_return([]) + + with pytest.raises(IncompatibleAddressableAreaError): + subject.get_addressable_area("abc") + + +def test_get_addressable_area_position() -> None: + """It should get the absolute location of the addressable area.""" + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={ + "abc": AddressableArea( + area_name="area", + display_name="fancy name", + bounding_box=Dimensions(x=10, y=20, z=30), + position=AddressableOffsetVector(x=1, y=2, z=3), + compatible_module_types=[], + drop_tip_location=None, + drop_labware_location=None, + ) + } + ) + + result = subject.get_addressable_area_position("abc") + assert result == Point(1, 2, 3) + + +def test_get_addressable_area_center() -> None: + """It should get the absolute location of an addressable area's center.""" + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={ + "abc": AddressableArea( + area_name="area", + display_name="fancy name", + bounding_box=Dimensions(x=10, y=20, z=30), + position=AddressableOffsetVector(x=1, y=2, z=3), + compatible_module_types=[], + drop_tip_location=None, + drop_labware_location=None, + ) + } + ) + + result = subject.get_addressable_area_center("abc") + assert result == Point(6, 12, 3) + + +def test_get_slot_definition() -> None: + """It should return a deck slot's definition.""" + subject = get_addressable_area_view( + loaded_addressable_areas_by_name={ + "6": AddressableArea( + area_name="area", + display_name="fancy name", + bounding_box=Dimensions(x=1, y=2, z=3), + position=AddressableOffsetVector(x=7, y=8, z=9), + compatible_module_types=["magneticModuleType"], + drop_tip_location=None, + drop_labware_location=None, + ) + } + ) + + result = subject.get_slot_definition(DeckSlotName.SLOT_6) + + assert result == { + "id": "area", + "position": [7, 8, 9], + "boundingBox": { + "xDimension": 1, + "yDimension": 2, + "zDimension": 3, + }, + "displayName": "fancy name", + "compatibleModuleTypes": ["magneticModuleType"], + } + + +# TODO Uncomment once Robot Server deck config and tests is hooked up +# def test_get_slot_definition_raises_with_bad_slot_name() -> None: +# """It should raise a SlotDoesNotExistError if a bad slot name is given.""" +# subject = get_addressable_area_view() +# +# with pytest.raises(SlotDoesNotExistError): +# subject.get_slot_definition(DeckSlotName.SLOT_A1) diff --git a/api/tests/opentrons/protocol_engine/state/test_command_store.py b/api/tests/opentrons/protocol_engine/state/test_command_store.py index 6a53ce46a61..a017df3b362 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_store.py @@ -681,7 +681,11 @@ def test_command_store_handles_pause_action(pause_source: PauseSource) -> None: def test_command_store_handles_play_action(pause_source: PauseSource) -> None: """It should set the running flag on play.""" subject = CommandStore(is_door_open=False, config=_make_config()) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) assert subject.state == CommandState( queue_status=QueueStatus.RUNNING, @@ -705,7 +709,11 @@ def test_command_store_handles_finish_action() -> None: """It should change to a succeeded state with FinishAction.""" subject = CommandStore(is_door_open=False, config=_make_config()) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) subject.handle_action(FinishAction()) assert subject.state == CommandState( @@ -730,7 +738,11 @@ def test_command_store_handles_finish_action_with_stopped() -> None: """It should change to a stopped state if FinishAction has set_run_status=False.""" subject = CommandStore(is_door_open=False, config=_make_config()) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) subject.handle_action(FinishAction(set_run_status=False)) assert subject.state.run_result == RunResult.STOPPED @@ -741,7 +753,11 @@ def test_command_store_handles_stop_action(from_estop: bool) -> None: """It should mark the engine as non-gracefully stopped on StopAction.""" subject = CommandStore(is_door_open=False, config=_make_config()) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) subject.handle_action(StopAction(from_estop=from_estop)) assert subject.state == CommandState( @@ -766,7 +782,11 @@ def test_command_store_cannot_restart_after_should_stop() -> None: """It should reject a play action after finish.""" subject = CommandStore(is_door_open=False, config=_make_config()) subject.handle_action(FinishAction()) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) assert subject.state == CommandState( queue_status=QueueStatus.PAUSED, @@ -792,7 +812,7 @@ def test_command_store_save_started_completed_run_timestamp() -> None: start_time = datetime(year=2021, month=1, day=1) hardware_stopped_time = datetime(year=2022, month=2, day=2) - subject.handle_action(PlayAction(requested_at=start_time)) + subject.handle_action(PlayAction(requested_at=start_time, deck_configuration=[])) subject.handle_action( HardwareStoppedAction( completed_at=hardware_stopped_time, finish_error_details=None @@ -812,9 +832,9 @@ def test_timestamps_are_latched() -> None: stop_time_1 = datetime(year=2023, month=3, day=3) stop_time_2 = datetime(year=2024, month=4, day=4) - subject.handle_action(PlayAction(requested_at=play_time_1)) + subject.handle_action(PlayAction(requested_at=play_time_1, deck_configuration=[])) subject.handle_action(PauseAction(source=PauseSource.CLIENT)) - subject.handle_action(PlayAction(requested_at=play_time_2)) + subject.handle_action(PlayAction(requested_at=play_time_2, deck_configuration=[])) subject.handle_action( HardwareStoppedAction(completed_at=stop_time_1, finish_error_details=None) ) @@ -979,7 +999,11 @@ def test_command_store_ignores_stop_after_graceful_finish() -> None: """It should no-op on stop if already gracefully finished.""" subject = CommandStore(is_door_open=False, config=_make_config()) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) subject.handle_action(FinishAction()) subject.handle_action(StopAction()) @@ -1005,7 +1029,11 @@ def test_command_store_ignores_finish_after_non_graceful_stop() -> None: """It should no-op on finish if already ungracefully stopped.""" subject = CommandStore(is_door_open=False, config=_make_config()) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) subject.handle_action(StopAction()) subject.handle_action(FinishAction()) @@ -1119,7 +1147,7 @@ def test_command_store_handles_play_according_to_initial_door_state( """It should set command queue state on play action according to door state.""" subject = CommandStore(is_door_open=is_door_open, config=config) start_time = datetime(year=2021, month=1, day=1) - subject.handle_action(PlayAction(requested_at=start_time)) + subject.handle_action(PlayAction(requested_at=start_time, deck_configuration=[])) assert subject.state.queue_status == expected_queue_status assert subject.state.run_started_at == start_time @@ -1162,7 +1190,11 @@ def test_handles_door_open_and_close_event_after_play( """It should update state when door opened and closed after run is played.""" subject = CommandStore(is_door_open=False, config=config) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) subject.handle_action(DoorChangeAction(door_state=DoorState.OPEN)) assert subject.state.queue_status == expected_queue_status diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view.py b/api/tests/opentrons/protocol_engine/state/test_command_view.py index c52180996f1..985ae5050f9 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_view.py @@ -326,19 +326,25 @@ class ActionAllowedSpec(NamedTuple): # play is allowed if the engine is idle ActionAllowedSpec( subject=get_command_view(queue_status=QueueStatus.SETUP), - action=PlayAction(requested_at=datetime(year=2021, month=1, day=1)), + action=PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ), expected_error=None, ), # play is allowed if engine is idle, even if door is blocking ActionAllowedSpec( subject=get_command_view(is_door_blocking=True, queue_status=QueueStatus.SETUP), - action=PlayAction(requested_at=datetime(year=2021, month=1, day=1)), + action=PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ), expected_error=None, ), # play is allowed if the engine is paused ActionAllowedSpec( subject=get_command_view(queue_status=QueueStatus.PAUSED), - action=PlayAction(requested_at=datetime(year=2021, month=1, day=1)), + action=PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ), expected_error=None, ), # pause is allowed if the engine is running @@ -369,13 +375,17 @@ class ActionAllowedSpec(NamedTuple): subject=get_command_view( is_door_blocking=True, queue_status=QueueStatus.PAUSED ), - action=PlayAction(requested_at=datetime(year=2021, month=1, day=1)), + action=PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ), expected_error=errors.RobotDoorOpenError, ), # play is disallowed if stop has been requested ActionAllowedSpec( subject=get_command_view(run_result=RunResult.STOPPED), - action=PlayAction(requested_at=datetime(year=2021, month=1, day=1)), + action=PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ), expected_error=errors.RunStoppedError, ), # pause is disallowed if stop has been requested diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 6f1d63cc829..4f1b5e764c2 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -40,6 +40,7 @@ from opentrons.protocol_engine.state.labware import LabwareView from opentrons.protocol_engine.state.modules import ModuleView from opentrons.protocol_engine.state.pipettes import PipetteView, StaticPipetteConfig +from opentrons.protocol_engine.state.addressable_areas import AddressableAreaView from opentrons.protocol_engine.state.geometry import GeometryView, _GripperMoveType @@ -61,6 +62,12 @@ def mock_pipette_view(decoy: Decoy) -> PipetteView: return decoy.mock(cls=PipetteView) +@pytest.fixture +def addressable_area_view(decoy: Decoy) -> AddressableAreaView: + """Get a mock in the shape of a AddressableAreaView.""" + return decoy.mock(cls=AddressableAreaView) + + @pytest.fixture(autouse=True) def patch_mock_move_types(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None: """Mock out move_types.py functions.""" @@ -70,7 +77,10 @@ def patch_mock_move_types(decoy: Decoy, monkeypatch: pytest.MonkeyPatch) -> None @pytest.fixture def subject( - labware_view: LabwareView, module_view: ModuleView, mock_pipette_view: PipetteView + labware_view: LabwareView, + module_view: ModuleView, + mock_pipette_view: PipetteView, + addressable_area_view: AddressableAreaView, ) -> GeometryView: """Get a GeometryView with its store dependencies mocked out.""" return GeometryView( @@ -81,12 +91,14 @@ def subject( labware_view=labware_view, module_view=module_view, pipette_view=mock_pipette_view, + addressable_area_view=addressable_area_view, ) def test_get_labware_parent_position( decoy: Decoy, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should return a deck slot position for labware in a deck slot.""" @@ -98,9 +110,9 @@ def test_get_labware_parent_position( offsetId=None, ) decoy.when(labware_view.get("labware-id")).then_return(labware_data) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - Point(1, 2, 3) - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(Point(1, 2, 3)) result = subject.get_labware_parent_position("labware-id") @@ -129,6 +141,7 @@ def test_get_labware_parent_position_on_module( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, ot2_standard_deck_def: DeckDefinitionV4, subject: GeometryView, ) -> None: @@ -145,9 +158,9 @@ def test_get_labware_parent_position_on_module( decoy.when(module_view.get_location("module-id")).then_return( DeckSlotLocation(slotName=DeckSlotName.SLOT_3) ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - Point(1, 2, 3) - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(Point(1, 2, 3)) decoy.when(labware_view.get_deck_definition()).then_return(ot2_standard_deck_def) decoy.when( module_view.get_nominal_module_offset( @@ -178,6 +191,7 @@ def test_get_labware_parent_position_on_labware( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, ot2_standard_deck_def: DeckDefinitionV4, subject: GeometryView, ) -> None: @@ -200,9 +214,9 @@ def test_get_labware_parent_position_on_labware( decoy.when(module_view.get_location("module-id")).then_return( DeckSlotLocation(slotName=DeckSlotName.SLOT_3) ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - Point(1, 2, 3) - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(Point(1, 2, 3)) decoy.when(labware_view.get("adapter-id")).then_return(adapter_data) decoy.when(labware_view.get_dimensions("adapter-id")).then_return( Dimensions(x=123, y=456, z=5) @@ -299,6 +313,7 @@ def test_get_labware_origin_position( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should return a deck slot position with the labware's offset as its origin.""" @@ -312,9 +327,9 @@ def test_get_labware_origin_position( decoy.when(labware_view.get("labware-id")).then_return(labware_data) decoy.when(labware_view.get_definition("labware-id")).then_return(well_plate_def) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - Point(1, 2, 3) - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(Point(1, 2, 3)) expected_parent = Point(1, 2, 3) expected_offset = Point( @@ -333,6 +348,7 @@ def test_get_labware_highest_z( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should get the absolute location of a labware's highest Z point.""" @@ -351,9 +367,9 @@ def test_get_labware_highest_z( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(slot_pos) highest_z = subject.get_labware_highest_z("labware-id") @@ -365,6 +381,7 @@ def test_get_module_labware_highest_z( well_plate_def: LabwareDefinition, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, ot2_standard_deck_def: DeckDefinitionV4, subject: GeometryView, ) -> None: @@ -384,9 +401,9 @@ def test_get_module_labware_highest_z( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(slot_pos) decoy.when(module_view.get_location("module-id")).then_return( DeckSlotLocation(slotName=DeckSlotName.SLOT_3) ) @@ -421,11 +438,13 @@ def test_get_all_labware_highest_z_no_equipment( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should return 0 if no loaded equipment.""" decoy.when(module_view.get_all()).then_return([]) decoy.when(labware_view.get_all()).then_return([]) + decoy.when(addressable_area_view.get_all()).then_return([]) result = subject.get_all_labware_highest_z() @@ -439,6 +458,7 @@ def test_get_all_labware_highest_z( falcon_tuberack_def: LabwareDefinition, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should get the highest Z amongst all labware.""" @@ -469,6 +489,7 @@ def test_get_all_labware_highest_z( reservoir_offset = LabwareOffsetVector(x=1, y=-2, z=3) decoy.when(module_view.get_all()).then_return([]) + decoy.when(addressable_area_view.get_all()).then_return([]) decoy.when(labware_view.get_all()).then_return([plate, off_deck_lw, reservoir]) decoy.when(labware_view.get("plate-id")).then_return(plate) @@ -491,12 +512,12 @@ def test_get_all_labware_highest_z( reservoir_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - Point(1, 2, 3) - ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - Point(4, 5, 6) - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(Point(1, 2, 3)) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(Point(4, 5, 6)) plate_z = subject.get_labware_highest_z("plate-id") reservoir_z = subject.get_labware_highest_z("reservoir-id") @@ -510,6 +531,7 @@ def test_get_all_labware_highest_z_with_modules( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should get the highest Z including modules.""" @@ -517,6 +539,8 @@ def test_get_all_labware_highest_z_with_modules( module_2 = LoadedModule.construct(id="module-id-2") # type: ignore[call-arg] decoy.when(labware_view.get_all()).then_return([]) + decoy.when(addressable_area_view.get_all()).then_return([]) + decoy.when(module_view.get_all()).then_return([module_1, module_2]) decoy.when(module_view.get_overall_height("module-id-1")).then_return(42.0) decoy.when(module_view.get_overall_height("module-id-2")).then_return(1337.0) @@ -526,6 +550,30 @@ def test_get_all_labware_highest_z_with_modules( assert result == 1337.0 +def test_get_all_labware_highest_z_with_addressable_area( + decoy: Decoy, + labware_view: LabwareView, + module_view: ModuleView, + addressable_area_view: AddressableAreaView, + subject: GeometryView, +) -> None: + """It should get the highest Z including addressable areas.""" + decoy.when(labware_view.get_all()).then_return([]) + decoy.when(module_view.get_all()).then_return([]) + + decoy.when(addressable_area_view.get_all()).then_return(["abc", "xyz"]) + decoy.when(addressable_area_view.get_addressable_area_height("abc")).then_return( + 42.0 + ) + decoy.when(addressable_area_view.get_addressable_area_height("xyz")).then_return( + 1337.0 + ) + + result = subject.get_all_labware_highest_z() + + assert result == 1337.0 + + @pytest.mark.parametrize( ["location", "min_z_height", "expected_min_z"], [ @@ -543,6 +591,7 @@ def test_get_min_travel_z( well_plate_def: LabwareDefinition, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, location: Optional[CurrentWell], min_z_height: Optional[float], expected_min_z: float, @@ -562,12 +611,13 @@ def test_get_min_travel_z( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( LabwareOffsetVector(x=0, y=0, z=3) ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_3)).then_return( - Point(0, 0, 3) - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_3.id) + ).then_return(Point(0, 0, 3)) decoy.when(module_view.get_all()).then_return([]) decoy.when(labware_view.get_all()).then_return([]) + decoy.when(addressable_area_view.get_all()).then_return([]) min_travel_z = subject.get_min_travel_z( "pipette-id", "labware-id", location, min_z_height @@ -580,6 +630,7 @@ def test_get_labware_position( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should return the slot position plus calibrated offset.""" @@ -598,9 +649,9 @@ def test_get_labware_position( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) position = subject.get_labware_position(labware_id="labware-id") @@ -615,6 +666,7 @@ def test_get_well_position( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should be able to get the position of a well top in a labware.""" @@ -634,9 +686,9 @@ def test_get_well_position( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) @@ -669,6 +721,7 @@ def test_get_module_labware_well_position( well_plate_def: LabwareDefinition, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, ot2_standard_deck_def: DeckDefinitionV4, subject: GeometryView, ) -> None: @@ -689,9 +742,9 @@ def test_get_module_labware_well_position( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) @@ -731,6 +784,7 @@ def test_get_well_position_with_top_offset( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should be able to get the position of a well top in a labware.""" @@ -750,9 +804,9 @@ def test_get_well_position_with_top_offset( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) @@ -777,6 +831,7 @@ def test_get_well_position_with_bottom_offset( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should be able to get the position of a well bottom in a labware.""" @@ -796,9 +851,9 @@ def test_get_well_position_with_bottom_offset( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) @@ -823,6 +878,7 @@ def test_get_well_position_with_center_offset( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should be able to get the position of a well center in a labware.""" @@ -842,9 +898,9 @@ def test_get_well_position_with_center_offset( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) @@ -869,6 +925,7 @@ def test_get_relative_well_location( decoy: Decoy, well_plate_def: LabwareDefinition, labware_view: LabwareView, + addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: """It should get the relative location of a well given an absolute position.""" @@ -888,9 +945,9 @@ def test_get_relative_well_location( decoy.when(labware_view.get_labware_offset_vector("labware-id")).then_return( calibration_offset ) - decoy.when(labware_view.get_slot_position(DeckSlotName.SLOT_4)).then_return( - slot_pos - ) + decoy.when( + addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) decoy.when(labware_view.get_well_definition("labware-id", "B2")).then_return( well_def ) @@ -1059,15 +1116,22 @@ def test_get_tip_drop_location_with_non_tiprack( ) -def test_get_tip_drop_explicit_location(subject: GeometryView) -> None: +def test_get_tip_drop_explicit_location( + decoy: Decoy, + labware_view: LabwareView, + subject: GeometryView, + tip_rack_def: LabwareDefinition, +) -> None: """It should pass the location through if origin is not WellOrigin.DROP_TIP.""" + decoy.when(labware_view.get_definition("tip-rack-id")).then_return(tip_rack_def) + input_location = DropTipWellLocation( origin=DropTipWellOrigin.TOP, offset=WellOffset(x=1, y=2, z=3), ) result = subject.get_checked_tip_drop_location( - pipette_id="pipette-id", labware_id="labware-id", well_location=input_location + pipette_id="pipette-id", labware_id="tip-rack-id", well_location=input_location ) assert result == WellLocation( @@ -1147,6 +1211,7 @@ def test_get_labware_grip_point( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, ot2_standard_deck_def: DeckDefinitionV4, subject: GeometryView, ) -> None: @@ -1155,9 +1220,9 @@ def test_get_labware_grip_point( labware_view.get_grip_height_from_labware_bottom("labware-id") ).then_return(100) - decoy.when(labware_view.get_slot_center_position(DeckSlotName.SLOT_1)).then_return( - Point(x=101, y=102, z=103) - ) + decoy.when( + addressable_area_view.get_addressable_area_center(DeckSlotName.SLOT_1.id) + ).then_return(Point(x=101, y=102, z=103)) labware_center = subject.get_labware_grip_point( labware_id="labware-id", location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1) ) @@ -1169,6 +1234,7 @@ def test_get_labware_grip_point_on_labware( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, ot2_standard_deck_def: DeckDefinitionV4, subject: GeometryView, ) -> None: @@ -1200,9 +1266,9 @@ def test_get_labware_grip_point_on_labware( labware_view.get_labware_overlap_offsets("labware-id", "below-name") ).then_return(OverlapOffset(x=0, y=1, z=6)) - decoy.when(labware_view.get_slot_center_position(DeckSlotName.SLOT_4)).then_return( - Point(x=5, y=9, z=10) - ) + decoy.when( + addressable_area_view.get_addressable_area_center(DeckSlotName.SLOT_4.id) + ).then_return(Point(x=5, y=9, z=10)) grip_point = subject.get_labware_grip_point( labware_id="labware-id", location=OnLabwareLocation(labwareId="below-id") @@ -1215,6 +1281,7 @@ def test_get_labware_grip_point_for_labware_on_module( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, ot2_standard_deck_def: DeckDefinitionV4, subject: GeometryView, ) -> None: @@ -1245,9 +1312,9 @@ def test_get_labware_grip_point_for_labware_on_module( location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), ) ) - decoy.when(labware_view.get_slot_center_position(DeckSlotName.SLOT_4)).then_return( - Point(100, 200, 300) - ) + decoy.when( + addressable_area_view.get_addressable_area_center(DeckSlotName.SLOT_4.id) + ).then_return(Point(100, 200, 300)) result_grip_point = subject.get_labware_grip_point( labware_id="labware-id", location=ModuleLocation(moduleId="module-id") ) @@ -1267,6 +1334,7 @@ def test_get_extra_waypoints( decoy: Decoy, labware_view: LabwareView, module_view: ModuleView, + addressable_area_view: AddressableAreaView, location: Optional[CurrentWell], should_dodge: bool, expected_waypoints: List[Tuple[float, float]], @@ -1297,7 +1365,9 @@ def test_get_extra_waypoints( ).then_return(should_dodge) decoy.when( # Assume the subject's Config is for an OT-3, so use an OT-3 slot name. - labware_view.get_slot_center_position(slot=DeckSlotName.SLOT_C2) + addressable_area_view.get_addressable_area_center( + addressable_area_name=DeckSlotName.SLOT_C2.id + ) ).then_return(Point(x=11, y=22, z=33)) extra_waypoints = subject.get_extra_waypoints("to-labware-id", location) diff --git a/api/tests/opentrons/protocol_engine/state/test_labware_view.py b/api/tests/opentrons/protocol_engine/state/test_labware_view.py index 2118ad36ade..885104c080c 100644 --- a/api/tests/opentrons/protocol_engine/state/test_labware_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_labware_view.py @@ -20,7 +20,7 @@ STANDARD_OT3_DECK, ) from opentrons.protocols.models import LabwareDefinition -from opentrons.types import DeckSlotName, Point, MountType +from opentrons.types import DeckSlotName, MountType from opentrons.protocol_engine import errors from opentrons.protocol_engine.types import ( @@ -805,45 +805,6 @@ def test_get_deck_definition(ot2_standard_deck_def: DeckDefinitionV4) -> None: assert subject.get_deck_definition() == ot2_standard_deck_def -def test_get_slot_definition(ot2_standard_deck_def: DeckDefinitionV4) -> None: - """It should return a deck slot's definition.""" - subject = get_labware_view(deck_definition=ot2_standard_deck_def) - - result = subject.get_slot_definition(DeckSlotName.SLOT_6) - - assert result["id"] == "6" - assert result["displayName"] == "Slot 6" - - -def test_get_slot_definition_raises_with_bad_slot_name( - ot2_standard_deck_def: DeckDefinitionV4, -) -> None: - """It should raise a SlotDoesNotExistError if a bad slot name is given.""" - subject = get_labware_view(deck_definition=ot2_standard_deck_def) - - with pytest.raises(errors.SlotDoesNotExistError): - subject.get_slot_definition(DeckSlotName.SLOT_A1) - - -def test_get_slot_position(ot2_standard_deck_def: DeckDefinitionV4) -> None: - """It should get the absolute location of a deck slot's origin.""" - subject = get_labware_view(deck_definition=ot2_standard_deck_def) - - expected_position = Point(x=132.5, y=90.5, z=0.0) - result = subject.get_slot_position(DeckSlotName.SLOT_5) - - assert result == expected_position - - -def test_get_slot_center_position(ot2_standard_deck_def: DeckDefinitionV4) -> None: - """It should get the absolute location of a deck slot's center.""" - subject = get_labware_view(deck_definition=ot2_standard_deck_def) - - expected_center = Point(x=196.5, y=43.0, z=0.0) - result = subject.get_slot_center_position(DeckSlotName.SLOT_2) - assert result == expected_center - - def test_get_labware_offset_vector() -> None: """It should get a labware's offset vector.""" labware_without_offset = LoadedLabware( @@ -1372,7 +1333,7 @@ def test_get_deck_gripper_offsets(ot3_standard_deck_def: DeckDefinitionV4) -> No assert subject.get_deck_default_gripper_offsets() == LabwareMovementOffsetData( pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), - dropOffset=LabwareOffsetVector(x=0, y=0, z=-0.25), + dropOffset=LabwareOffsetVector(x=0, y=0, z=-0.75), ) diff --git a/api/tests/opentrons/protocol_engine/state/test_module_view.py b/api/tests/opentrons/protocol_engine/state/test_module_view.py index 03d06ef6d36..e4498c0ec7d 100644 --- a/api/tests/opentrons/protocol_engine/state/test_module_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_module_view.py @@ -1723,14 +1723,14 @@ def test_is_edge_move_unsafe( lazy_fixture("thermocycler_v2_def"), LabwareMovementOffsetData( pickUpOffset=LabwareOffsetVector(x=0, y=0, z=4.6), - dropOffset=LabwareOffsetVector(x=0, y=0, z=4.6), + dropOffset=LabwareOffsetVector(x=0, y=0, z=5.6), ), ), ( lazy_fixture("heater_shaker_v1_def"), LabwareMovementOffsetData( pickUpOffset=LabwareOffsetVector(x=0, y=0, z=0), - dropOffset=LabwareOffsetVector(x=0, y=0, z=0.5), + dropOffset=LabwareOffsetVector(x=0, y=0, z=1.0), ), ), ( diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py index 25370a410fc..3f638991c95 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py @@ -69,6 +69,7 @@ def test_sets_initial_state(subject: PipetteStore) -> None: movement_speed_by_id={}, static_config_by_id={}, flow_rates_by_id={}, + nozzle_configuration_by_id={}, ) diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py index b76ba20303f..5721beb5b18 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py @@ -24,6 +24,7 @@ HardwarePipette, StaticPipetteConfig, ) +from opentrons.hardware_control.nozzle_manager import NozzleMap from opentrons.protocol_engine.errors import TipNotAttachedError, PipetteNotLoadedError @@ -38,6 +39,7 @@ def get_pipette_view( movement_speed_by_id: Optional[Dict[str, Optional[float]]] = None, static_config_by_id: Optional[Dict[str, StaticPipetteConfig]] = None, flow_rates_by_id: Optional[Dict[str, FlowRates]] = None, + nozzle_layout_by_id: Optional[Dict[str, Optional[NozzleMap]]] = None, ) -> PipetteView: """Get a pipette view test subject with the specified state.""" state = PipetteState( @@ -49,6 +51,7 @@ def get_pipette_view( movement_speed_by_id=movement_speed_by_id or {}, static_config_by_id=static_config_by_id or {}, flow_rates_by_id=flow_rates_by_id or {}, + nozzle_configuration_by_id=nozzle_layout_by_id or {}, ) return PipetteView(state=state) diff --git a/api/tests/opentrons/protocol_engine/state/test_state_store.py b/api/tests/opentrons/protocol_engine/state/test_state_store.py index 44c42fe50b2..41148eb006c 100644 --- a/api/tests/opentrons/protocol_engine/state/test_state_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_state_store.py @@ -55,7 +55,11 @@ def test_has_state(subject: StateStore) -> None: def test_state_is_immutable(subject: StateStore) -> None: """It should treat the state as immutable.""" result_1 = subject.state - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) result_2 = subject.state assert result_1 is not result_2 @@ -68,7 +72,11 @@ def test_notify_on_state_change( ) -> None: """It should notify state changes when actions are handled.""" decoy.verify(change_notifier.notify(), times=0) - subject.handle_action(PlayAction(requested_at=datetime(year=2021, month=1, day=1))) + subject.handle_action( + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) + ) decoy.verify(change_notifier.notify(), times=1) diff --git a/api/tests/opentrons/protocol_engine/test_plugins.py b/api/tests/opentrons/protocol_engine/test_plugins.py index 0da44ab62bc..471a689e265 100644 --- a/api/tests/opentrons/protocol_engine/test_plugins.py +++ b/api/tests/opentrons/protocol_engine/test_plugins.py @@ -29,7 +29,9 @@ def test_configure( decoy: Decoy, state_view: StateView, action_dispatcher: ActionDispatcher ) -> None: """The engine should be able to configure the plugin.""" - action = PlayAction(requested_at=datetime(year=2021, month=1, day=1)) + action = PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) subject = _MyPlugin() subject._configure( diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index d8928126495..156c68aee8b 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -343,15 +343,23 @@ def test_play( ) decoy.when( state_store.commands.validate_action_allowed( - PlayAction(requested_at=datetime(year=2021, month=1, day=1)) + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) ), - ).then_return(PlayAction(requested_at=datetime(year=2022, month=2, day=2))) + ).then_return( + PlayAction( + requested_at=datetime(year=2022, month=2, day=2), deck_configuration=[] + ) + ) - subject.play() + subject.play(deck_configuration=[]) decoy.verify( action_dispatcher.dispatch( - PlayAction(requested_at=datetime(year=2022, month=2, day=2)) + PlayAction( + requested_at=datetime(year=2022, month=2, day=2), deck_configuration=[] + ) ), hardware_api.resume(HardwarePauseType.PAUSE), ) @@ -371,17 +379,25 @@ def test_play_blocked_by_door( ) decoy.when( state_store.commands.validate_action_allowed( - PlayAction(requested_at=datetime(year=2021, month=1, day=1)) + PlayAction( + requested_at=datetime(year=2021, month=1, day=1), deck_configuration=[] + ) ), - ).then_return(PlayAction(requested_at=datetime(year=2022, month=2, day=2))) + ).then_return( + PlayAction( + requested_at=datetime(year=2022, month=2, day=2), deck_configuration=[] + ) + ) decoy.when(state_store.commands.get_is_door_blocking()).then_return(True) - subject.play() + subject.play(deck_configuration=[]) decoy.verify(hardware_api.resume(HardwarePauseType.PAUSE), times=0) decoy.verify( action_dispatcher.dispatch( - PlayAction(requested_at=datetime(year=2022, month=2, day=2)) + PlayAction( + requested_at=datetime(year=2022, month=2, day=2), deck_configuration=[] + ) ), hardware_api.pause(HardwarePauseType.PAUSE), ) diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py index c5e381e56a1..5baf821ca91 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_command_mapper.py @@ -36,7 +36,7 @@ async def simulate_and_get_commands(protocol_file: Path) -> List[commands.Comman robot_type="OT-2 Standard", protocol_config=protocol_source.config, ) - result = await subject.run(protocol_source) + result = await subject.run(deck_configuration=[], protocol_source=protocol_source) assert result.state_summary.errors == [] assert result.state_summary.status == EngineStatus.SUCCEEDED return result.commands diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py index 32456b98af1..e9c9371fe2a 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_custom_labware.py @@ -56,7 +56,7 @@ async def test_legacy_custom_labware(custom_labware_protocol_files: List[Path]) robot_type="OT-2 Standard", protocol_config=protocol_source.config, ) - result = await subject.run(protocol_source) + result = await subject.run(deck_configuration=[], protocol_source=protocol_source) expected_labware = LoadedLabware.construct( id=matchers.Anything(), diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py index dd7d74885fe..b7f80506593 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_legacy_module_commands.py @@ -64,7 +64,7 @@ async def test_runner_with_modules_in_legacy_python( robot_type="OT-2 Standard", protocol_config=protocol_source.config, ) - result = await subject.run(protocol_source) + result = await subject.run(deck_configuration=[], protocol_source=protocol_source) commands_result = result.commands assert len(commands_result) == 6 diff --git a/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py index 459361a5972..d1925e3f93c 100644 --- a/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/smoke_tests/test_protocol_runner.py @@ -43,7 +43,7 @@ async def test_runner_with_python( robot_type="OT-2 Standard", protocol_config=protocol_source.config, ) - result = await subject.run(protocol_source) + result = await subject.run(deck_configuration=[], protocol_source=protocol_source) commands_result = result.commands pipettes_result = result.state_summary.pipettes labware_result = result.state_summary.labware @@ -114,7 +114,7 @@ async def test_runner_with_json(json_protocol_file: Path) -> None: subject = await create_simulating_runner( robot_type="OT-2 Standard", protocol_config=protocol_source.config ) - result = await subject.run(protocol_source) + result = await subject.run(deck_configuration=[], protocol_source=protocol_source) commands_result = result.commands pipettes_result = result.state_summary.pipettes @@ -176,7 +176,7 @@ async def test_runner_with_legacy_python(legacy_python_protocol_file: Path) -> N robot_type="OT-2 Standard", protocol_config=protocol_source.config, ) - result = await subject.run(protocol_source) + result = await subject.run(deck_configuration=[], protocol_source=protocol_source) commands_result = result.commands pipettes_result = result.state_summary.pipettes @@ -235,7 +235,7 @@ async def test_runner_with_legacy_json(legacy_json_protocol_file: Path) -> None: subject = await create_simulating_runner( robot_type="OT-2 Standard", protocol_config=protocol_source.config ) - result = await subject.run(protocol_source) + result = await subject.run(deck_configuration=[], protocol_source=protocol_source) commands_result = result.commands pipettes_result = result.state_summary.pipettes diff --git a/api/tests/opentrons/protocol_runner/test_protocol_runner.py b/api/tests/opentrons/protocol_runner/test_protocol_runner.py index 7965fc3bc1f..0e4f4ec31ca 100644 --- a/api/tests/opentrons/protocol_runner/test_protocol_runner.py +++ b/api/tests/opentrons/protocol_runner/test_protocol_runner.py @@ -210,9 +210,9 @@ async def test_play_starts_run( subject: AnyRunner, ) -> None: """It should start a protocol run with play.""" - subject.play() + subject.play(deck_configuration=[]) - decoy.verify(protocol_engine.play(), times=1) + decoy.verify(protocol_engine.play(deck_configuration=[]), times=1) @pytest.mark.parametrize( @@ -299,11 +299,11 @@ async def test_run_json_runner( ) assert json_runner_subject.was_started() is False - await json_runner_subject.run() + await json_runner_subject.run(deck_configuration=[]) assert json_runner_subject.was_started() is True decoy.verify( - protocol_engine.play(), + protocol_engine.play(deck_configuration=[]), task_queue.start(), await task_queue.join(), ) @@ -616,11 +616,11 @@ async def test_run_python_runner( ) assert legacy_python_runner_subject.was_started() is False - await legacy_python_runner_subject.run() + await legacy_python_runner_subject.run(deck_configuration=[]) assert legacy_python_runner_subject.was_started() is True decoy.verify( - protocol_engine.play(), + protocol_engine.play(deck_configuration=[]), task_queue.start(), await task_queue.join(), ) @@ -639,12 +639,12 @@ async def test_run_live_runner( ) assert live_runner_subject.was_started() is False - await live_runner_subject.run() + await live_runner_subject.run(deck_configuration=[]) assert live_runner_subject.was_started() is True decoy.verify( await hardware_api.home(), - protocol_engine.play(), + protocol_engine.play(deck_configuration=[]), task_queue.start(), await task_queue.join(), ) diff --git a/app-shell-odd/src/system-update/index.ts b/app-shell-odd/src/system-update/index.ts index c0ea2f49ee3..15f64186e0d 100644 --- a/app-shell-odd/src/system-update/index.ts +++ b/app-shell-odd/src/system-update/index.ts @@ -212,7 +212,9 @@ const getVersionFromZipIfValid = (path: string): Promise => const fakeReleaseNotesForMassStorage = (version: string): string => ` # Opentrons Robot Software Version ${version} -This update is from a USB mass storage device connected to your flex, and release notes cannot be shown. +This update is from a USB mass storage device connected to your Flex, and release notes cannot be shown. + +Don't remove the USB mass storage device while the update is in progress. ` export const getLatestMassStorageUpdateFiles = ( diff --git a/app-shell/build/release-notes-internal.md b/app-shell/build/release-notes-internal.md index 08663572808..a15d877c0ab 100644 --- a/app-shell/build/release-notes-internal.md +++ b/app-shell/build/release-notes-internal.md @@ -3,26 +3,21 @@ For more details about this release, please see the full [technical changelog][] --- -# Internal Release 0.14.0 +# Internal Release 1.1.0 -This is 0.14.0, an internal release for the app supporting the Opentrons Flex. +This is 1.0.0, an internal release for the app supporting the Opentrons Flex. This is still pretty early in the process, so some things are known not to work, and are listed below. Some things that may surprise you do work, and are also listed below. There may also be some littler things of note, and those are at the bottom. ## New Stuff In This Release -- All instrument flows should display errors properly now -- Update robot flows don't say OT-2s anymore -- There should be fewer surprise scroll bars on Windows -- The configuration of the on-device display can be factory-reset, which lets you go back to the first-time setup flow +- There is now UI for configuring the loaded deck fixtures such as trash chutes on your Flex. +- Support for analyzing python protocol API 2.16 and JSON protocol V8 +- Labware position check now uses the calibration (the same one used for pipette and module calibration) instead of a tip; this should increase the accuracy of LPC. +- Connecting a Flex to a wifi network while the app is connected to it with USB should work now +- The app should generally be better about figuring out what kind of robot a protocol is for, and displaying the correct deck layout accordingly +## Known Issues -## Big Things That Do Work Please Do Report Bugs About Them -- Connecting to a Flex, including via USB -- Running protocols on those Flexs including simulate, play/pause, labware position check - - Except for gripper; no LPC for gripper -- Attach, detach, and calibration flows for pipettes and gripper when started from the device page -- Automatic updates of app and robot when new internal-releases are created - - +- Labware Renders are slightly askew towards the top right. diff --git a/app-shell/src/dialogs/index.ts b/app-shell/src/dialogs/index.ts index 5b2ef9f2b24..92e59defb39 100644 --- a/app-shell/src/dialogs/index.ts +++ b/app-shell/src/dialogs/index.ts @@ -11,6 +11,17 @@ interface BaseDialogOptions { interface FileDialogOptions extends BaseDialogOptions { filters: Array<{ name: string; extensions: string[] }> + properties: Array< + | 'openDirectory' + | 'createDirectory' + | 'openFile' + | 'multiSelections' + | 'showHiddenFiles' + | 'promptToCreate' + | 'noResolveAliases' + | 'treatPackageAsDirectory' + | 'dontAddToRecent' + > } const BASE_DIRECTORY_OPTS = { @@ -55,6 +66,13 @@ export function showOpenFileDialog( openDialogOpts = { ...openDialogOpts, filters: options.filters } } + if (options.properties != null) { + openDialogOpts = { + ...openDialogOpts, + properties: [...(openDialogOpts.properties ?? []), ...options.properties], + } + } + return dialog .showOpenDialog(browserWindow, openDialogOpts) .then((result: OpenDialogReturnValue) => { diff --git a/app-shell/src/http.ts b/app-shell/src/http.ts index c4ce12d4aa6..02fe50da3e1 100644 --- a/app-shell/src/http.ts +++ b/app-shell/src/http.ts @@ -90,17 +90,22 @@ export function postFile( init?: RequestInit, progress?: (progress: number) => void ): Promise { - return createReadStream(source, progress ?? null).then(readStream => { - const body = new FormData() - body.append(name, readStream) - return fetch(input, { ...init, body, method: 'POST' }) + return new Promise((resolve, reject) => { + createReadStream(source, progress ?? null, reject).then(readStream => { + return new Promise(resolve => { + const body = new FormData() + body.append(name, readStream) + resolve(fetch(input, { ...init, body, method: 'POST' })) + }).then(resolve) + }) }) } function createReadStreamWithSize( source: string, size: number, - progress: ((progress: number) => void) | null + progress: ((progress: number) => void) | null, + onError: (error: unknown) => unknown ): Promise { return new Promise((resolve, reject) => { const readStream = fs.createReadStream(source) @@ -125,6 +130,7 @@ function createReadStreamWithSize( } readStream.once('error', handleError) + readStream.once('error', onError) function handleSuccess(): void { resolve(readStream) @@ -142,12 +148,13 @@ function createReadStreamWithSize( // create a read stream, handling errors that `fetch` is unable to catch function createReadStream( source: string, - progress: ((progress: number) => void) | null + progress: ((progress: number) => void) | null, + onError: (error: unknown) => unknown ): Promise { return fsPromises .stat(source) .then(filestats => - createReadStreamWithSize(source, filestats.size, progress) + createReadStreamWithSize(source, filestats.size, progress, onError) ) - .catch(() => createReadStreamWithSize(source, Infinity, progress)) + .catch(() => createReadStreamWithSize(source, Infinity, progress, onError)) } diff --git a/app-shell/src/labware/__tests__/dispatch.test.ts b/app-shell/src/labware/__tests__/dispatch.test.ts index c7a8e9198d5..f88f271956d 100644 --- a/app-shell/src/labware/__tests__/dispatch.test.ts +++ b/app-shell/src/labware/__tests__/dispatch.test.ts @@ -229,7 +229,13 @@ describe('labware module dispatches', () => { return flush().then(() => { expect(showOpenFileDialog).toHaveBeenCalledWith(mockMainWindow, { defaultPath: '__mock-app-path__', - filters: [{ name: 'JSON Labware Definitions', extensions: ['json'] }], + filters: [ + { + name: 'JSON Labware Definitions', + extensions: ['json'], + }, + ], + properties: ['multiSelections'], }) expect(dispatch).not.toHaveBeenCalled() }) diff --git a/app-shell/src/labware/index.ts b/app-shell/src/labware/index.ts index e7adcc1e4ae..f46f9134527 100644 --- a/app-shell/src/labware/index.ts +++ b/app-shell/src/labware/index.ts @@ -158,6 +158,7 @@ export function registerLabware( filters: [ { name: 'JSON Labware Definitions', extensions: ['json'] }, ], + properties: ['multiSelections' as const], } addLabwareTask = showOpenFileDialog(mainWindow, dialogOptions).then( diff --git a/app-shell/src/robot-update/index.ts b/app-shell/src/robot-update/index.ts index b71eab6d44b..4f4d2bc8350 100644 --- a/app-shell/src/robot-update/index.ts +++ b/app-shell/src/robot-update/index.ts @@ -137,20 +137,28 @@ export function registerRobotUpdate(dispatch: Dispatch): Dispatch { case 'robotUpdate:READ_USER_FILE': { const { systemFile } = action.payload as { systemFile: string } - readFileAndDispatchInfo(dispatch, systemFile, true) - break + return readFileAndDispatchInfo(dispatch, systemFile, true) } case 'robotUpdate:READ_SYSTEM_FILE': { const { target } = action.payload const filename = updateSet[target]?.system + if (filename == null) { - return dispatch({ - type: 'robotUpdate:UNEXPECTED_ERROR', - payload: { message: 'Robot update file not downloaded' }, - }) + if (checkingForUpdates) { + dispatch({ + type: 'robotUpdate:CHECKING_FOR_UPDATE', + payload: target, + }) + } else { + // If the file was downloaded but deleted from robot-update-cache. + dispatch({ + type: 'robotUpdate:UNEXPECTED_ERROR', + payload: { message: 'Robot update file not downloaded' }, + }) + } } else { - readFileAndDispatchInfo(dispatch, filename) + return readFileAndDispatchInfo(dispatch, filename) } } } @@ -213,7 +221,7 @@ export function checkForRobotUpdate( const handleProgress = (progress: DownloadProgress): void => { const { downloaded, size } = progress if (size !== null) { - const percentDone = Math.round(downloaded / size) * 100 + const percentDone = Math.round((downloaded / size) * 100) if (percentDone - prevPercentDone > 0) { dispatch({ type: 'robotUpdate:DOWNLOAD_PROGRESS', @@ -227,7 +235,15 @@ export function checkForRobotUpdate( const targetDownloadDir = cacheDirForMachineFiles(target) return ensureDir(targetDownloadDir) - .then(() => getReleaseFiles(urls, targetDownloadDir, handleProgress)) + .then(() => + getReleaseFiles( + urls, + targetDownloadDir, + dispatch, + target, + handleProgress + ) + ) .then(filepaths => cacheUpdateSet(filepaths, target)) .then(updateInfo => dispatch({ type: 'robotUpdate:UPDATE_INFO', payload: updateInfo }) diff --git a/app-shell/src/robot-update/release-files.ts b/app-shell/src/robot-update/release-files.ts index d2d9d6b47cc..0c84634eb59 100644 --- a/app-shell/src/robot-update/release-files.ts +++ b/app-shell/src/robot-update/release-files.ts @@ -3,12 +3,17 @@ import assert from 'assert' import path from 'path' import { promisify } from 'util' import tempy from 'tempy' -import { move, readdir, remove } from 'fs-extra' +import { move, readdir, remove, readFile } from 'fs-extra' import StreamZip from 'node-stream-zip' import getStream from 'get-stream' +import { RobotUpdateTarget } from '@opentrons/app/src/redux/robot-update/types' + import { createLogger } from '../log' import { fetchToFile } from '../http' +import { Dispatch } from '../types' +import { CURRENT_VERSION } from '../update' + import type { DownloadProgress } from '../http' import type { ReleaseSetUrls, ReleaseSetFilepaths, UserFileInfo } from './types' @@ -23,6 +28,8 @@ const outPath = (dir: string, url: string): string => export function getReleaseFiles( urls: ReleaseSetUrls, directory: string, + dispatch: Dispatch, + target: RobotUpdateTarget, onProgress: (progress: DownloadProgress) => unknown ): Promise { return readdir(directory) @@ -44,41 +51,65 @@ export function getReleaseFiles( return { system, releaseNotes } } - return downloadReleaseFiles(urls, directory, onProgress) + return Promise.all([ + downloadAndNotify(true, urls.releaseNotes, directory, dispatch, target), + downloadAndNotify( + false, + urls.system, + directory, + dispatch, + target, + onProgress + ), + ]).then(([releaseNotes, system]) => ({ releaseNotes, system })) }) } -// downloads the entire release set to a temporary directory, and once they're -// all successfully downloaded, renames the directory to `directory` +// downloads robot update files to a temporary directory, and once +// successfully downloaded, renames the directory to `directory` // TODO(mc, 2019-07-09): DRY this up if/when more than 2 files are required -export function downloadReleaseFiles( - urls: ReleaseSetUrls, +export function downloadAndNotify( + isReleaseNotesDownload: boolean, + url: ReleaseSetUrls['releaseNotes' | 'system'], directory: string, + dispatch: Dispatch, + target: RobotUpdateTarget, // `onProgress` will be called with download progress as the files are read - onProgress: (progress: DownloadProgress) => unknown -): Promise { + onProgress?: (progress: DownloadProgress) => unknown +): Promise { const tempDir: string = tempy.directory() - const tempSystemPath = outPath(tempDir, urls.system) - const tempNotesPath = outPath(tempDir, urls.releaseNotes) + const tempPath = outPath(tempDir, url) + const path = outPath(directory, tempPath) + const logMessage = isReleaseNotesDownload ? 'release notes' : 'system files' - log.debug('directory created for robot update downloads', { tempDir }) + log.debug('directory created for ' + logMessage, { tempDir }) // downloads are streamed directly to the filesystem to avoid loading them // all into memory simultaneously - const systemReq = fetchToFile(urls.system, tempSystemPath, { onProgress }) - const notesReq = fetchToFile(urls.releaseNotes, tempNotesPath) - - return Promise.all([systemReq, notesReq]).then(results => { - const [systemTemp, releaseNotesTemp] = results - const systemPath = outPath(directory, systemTemp) - const notesPath = outPath(directory, releaseNotesTemp) - - log.debug('renaming directory', { from: tempDir, to: directory }) + const req = fetchToFile(url, tempPath, { + onProgress, + }) - return move(tempDir, directory, { overwrite: true }).then(() => ({ - system: systemPath, - releaseNotes: notesPath, - })) + return req.then(() => { + return move(tempPath, path, { overwrite: true }) + .then(() => { + if (isReleaseNotesDownload) { + return readFile(path, 'utf8').then(releaseNotes => + dispatch({ + type: 'robotUpdate:UPDATE_INFO', + payload: { releaseNotes, target, version: CURRENT_VERSION }, + }) + ) + } + // This action will only have an effect if the user is actively waiting for the download to complete. + else { + return dispatch({ + type: 'robotUpdate:DOWNLOAD_DONE', + payload: target, + }) + } + }) + .then(() => path) }) } diff --git a/app/src/assets/images/lpc_level_probe_with_labware.svg b/app/src/assets/images/lpc_level_probe_with_labware.svg new file mode 100644 index 00000000000..5e75128d9fc --- /dev/null +++ b/app/src/assets/images/lpc_level_probe_with_labware.svg @@ -0,0 +1,315 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/assets/images/lpc_level_probe_with_tip.svg b/app/src/assets/images/lpc_level_probe_with_tip.svg new file mode 100644 index 00000000000..799e78dd3d1 --- /dev/null +++ b/app/src/assets/images/lpc_level_probe_with_tip.svg @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index f717a04bac9..0f5ea5a8609 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -70,6 +70,7 @@ "problem_during_update": "This update is taking longer than usual.", "prompt": "Always show the prompt to choose calibration block or trash bin", "receive_alert": "Receive an alert when an Opentrons software update is available.", + "release_notes": "Release notes", "remind_later": "Remind me later", "reset_to_default": "Reset to default", "restart_touchscreen": "Restart touchscreen", diff --git a/app/src/assets/localization/en/device_details.json b/app/src/assets/localization/en/device_details.json index 7dfdc46a6a6..2a0c80d349c 100644 --- a/app/src/assets/localization/en/device_details.json +++ b/app/src/assets/localization/en/device_details.json @@ -33,7 +33,8 @@ "current_temp": "Current: {{temp}} °C", "current_version": "Current Version", "deck_cal_missing": "Pipette Offset calibration missing. Calibrate deck first.", - "deck_configuration_is_not_available": "Deck configuration is not available when run is in progress", + "deck_configuration_is_not_available_when_robot_is_busy": "Deck configuration is not available when the robot is busy", + "deck_configuration_is_not_available_when_run_is_in_progress": "Deck configuration is not available when run is in progress", "deck_configuration": "deck configuration", "deck_fixture_setup_instructions": "Deck fixture setup instructions", "deck_fixture_setup_modal_bottom_description_desktop": "For detailed instructions for different types of fixtures, scan the QR code or go to the link below.", @@ -78,13 +79,13 @@ "magdeck_gen1_height": "Height: {{height}}", "magdeck_gen2_height": "Height: {{height}} mm", "max_engage_height": "Max Engage Height", + "missing_fixture": "missing {{num}} fixture", + "missing_fixtures_plural": "missing {{count}} fixtures", "missing_hardware": "missing hardware", - "missing_module_plural": "missing {{count}} modules", - "missing_module": "missing {{num}} module", "missing_instrument": "missing {{num}} instrument", "missing_instruments_plural": "missing {{count}} instruments", - "missing_fixture": "missing {{num}} fixture", - "missing_fixtures_plural": "missing {{count}} fixtures", + "missing_module_plural": "missing {{count}} modules", + "missing_module": "missing {{num}} module", "module_actions_unavailable": "Module actions unavailable while protocol is running", "module_calibration_required_no_pipette_attached": "Module calibration required. Attach a pipette before running module calibration.", "module_calibration_required_update_pipette_FW": "Update pipette firmware before proceeding with required module calibration.", @@ -97,6 +98,7 @@ "mount": "{{side}} Mount", "na_speed": "Target: N/A", "na_temp": "Target: N/A", + "no_deck_fixtures": "No deck fixtures", "no_protocol_runs": "No protocol runs yet!", "no_protocols_found": "No protocols found", "no_recent_runs_description": "After you run some protocols, they will appear here.", diff --git a/app/src/assets/localization/en/device_settings.json b/app/src/assets/localization/en/device_settings.json index 2335abe9bd4..f530ad9f711 100644 --- a/app/src/assets/localization/en/device_settings.json +++ b/app/src/assets/localization/en/device_settings.json @@ -83,6 +83,7 @@ "device_reset_description": "Reset labware calibration, boot scripts, and/or robot calibration to factory settings.", "device_reset_slideout_description": "Select individual settings to only clear specific data types.", "device_resets_cannot_be_undone": "Resets cannot be undone", + "release_notes": "Release notes", "directly_connected_to_this_computer": "Directly connected to this computer.", "disconnect": "Disconnect", "disconnect_from_ssid": "Disconnect from {{ssid}}", diff --git a/app/src/assets/localization/en/labware_position_check.json b/app/src/assets/localization/en/labware_position_check.json index be73b09f6b9..f08c465f7fa 100644 --- a/app/src/assets/localization/en/labware_position_check.json +++ b/app/src/assets/localization/en/labware_position_check.json @@ -6,21 +6,27 @@ "applied_offset_data": "Applied Labware Offset data", "apply_offset_data": "Apply labware offset data", "apply_offsets": "apply offsets", - "attach_probe": "Attach probe to pipette", + "attach_probe": "Attach calibration probe", + "backmost": "backmost", + "calibration_probe": "calibration probe", "check_item_in_location": "Check {{item}} in {{location}}", "check_labware_in_slot_title": "Check Labware {{labware_display_name}} in slot {{slot}}", "check_remaining_labware_with_primary_pipette_section": "Check remaining labware with {{primary_mount}} Pipette and tip", + "check_tip_location": "the top of the tip in the A1 position", + "check_well_location": "well A1 on the labware", "clear_all_slots": "Clear all deck slots of labware, leaving modules in place", + "clear_all_slots_odd": "Clear all deck slots of labware", "cli_ssh": "Command Line Interface (SSH)", "close_and_apply_offset_data": "Close and apply labware offset data", + "confirm_detached": "Confirm removal", "confirm_pick_up_tip_modal_title": "Did the pipette pick up a tip successfully?", "confirm_pick_up_tip_modal_try_again_text": "No, try again", "confirm_position_and_move": "Confirm position, move to slot {{next_slot}}", "confirm_position_and_pick_up_tip": "Confirm position, pick up tip", "confirm_position_and_return_tip": "Confirm position, return tip to Slot {{next_slot}} and home", - "ensure_nozzle_is_above_tip_odd": "Ensure that the pipette nozzle furthest from you is centered above and level with the top of the tip in the A1 position. If it isn't, tap Move pipette and then jog the pipette until it is properly aligned.", - "ensure_nozzle_is_above_tip_desktop": "Ensure that the pipette nozzle furthest from you is centered above and level with the top of the tip in the A1 position. If it isn't, use the controls below or your keyboard to jog the pipette until it is properly aligned.", - "ensure_tip_is_above_well": "Ensure that the pipette tip furthest from you is centered above and level with well A1 on the labware.", + "detach_probe": "Remove calibration probe", + "ensure_nozzle_position_odd": "Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, tap Move pipette and then jog the pipette until it is properly aligned.", + "ensure_nozzle_position_desktop": "Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, use the controls below or your keyboard to jog the pipette until it is properly aligned.", "error_modal_header": "Something went wrong", "error_modal_problem_in_app": "There was an error performing Labware Position Check. Please restart the app. If the problem persists, please contact Opentrons Support", "error_modal_problem_on_robot": "There was an error processing your request on the robot", @@ -29,8 +35,7 @@ "exit_screen_subtitle": "If you exit now, all labware offsets will be discarded. This cannot be undone.", "exit_screen_title": "Exit before completing Labware Position Check?", "get_labware_offset_data": "Get Labware Offset Data", - "install_probe_8_channel": "Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the backmost pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.", - "install_probe_96_channel": "Take the calibration probe from its storage location. Ensure its collar is unlocked. Push the pipette ejector up and press the probe firmly onto the A1 (back left corner) pipette nozzle. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.", + "install_probe": "Take the calibration probe from its storage location. Ensure its collar is fully unlocked. Push the pipette ejector up and press the probe firmly onto the {{location}} pipette nozzle as far as it can go. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.", "jog_controls_adjustment": "Need to make an adjustment?", "jupyter_notebook": "Jupyter Notebook", "labware_display_location_text": "Deck Slot {{slot}}", @@ -69,6 +74,7 @@ "move_to_a1_position": "Move the pipette to line up in the A1 position", "moving_to_slot_title": "Moving to slot {{slot}}", "new_labware_offset_data": "New labware offset data", + "ninety_six_probe_location": "A1 (back left corner)", "no_labware_offsets": "No Labware Offset", "no_offset_data_available": "No labware offset data available", "no_offset_data_on_robot": "This robot has no useable labware offset data for this run.", @@ -76,6 +82,7 @@ "offsets": "offsets", "pick_up_tip_from_rack_in_location": "Pick up tip from tip rack in {{location}}", "picking_up_tip_title": "Picking up tip in slot {{slot}}", + "pipette_nozzle": "pipette nozzle furthest from you", "place_a_full_tip_rack_in_location": "Place a full {{tip_rack}} into {{location}}", "place_labware_in_adapter_in_location": "Place a {{adapter}} followed by a {{labware}} into {{location}}", "place_labware_in_location": "Place a {{labware}} into {{location}}", @@ -84,6 +91,9 @@ "position_check_description": "Labware Position Check is a guided workflow that checks every labware on the deck for an added degree of precision in your protocol.When you check a labware, the OT-2’s pipette nozzle or attached tip will stop at the center of the A1 well. If the pipette nozzle or tip is not centered, you can reveal the OT-2’s jog controls to make an adjustment. This Labware Offset will be applied to the entire labware. Offset data is measured to the nearest 1/10th mm and can be made in the X, Y and/or Z directions.", "prepare_item_in_location": "Prepare {{item}} in {{location}}", "primary_pipette_tipracks_section": "Check tip racks with {{primary_mount}} Pipette and pick up a tip", + "remove_calibration_probe": "Remove calibration probe", + "remove_probe": "Unlock the calibraiton probe, remove it from the nozzle, and return it to its storage location.", + "remove_probe_before_exit": "Remove the calibration probe before exiting", "return_tip_rack_to_location": "Return tip rack to {{location}}", "return_tip_section": "Return tip", "returning_tip_title": "Returning tip in slot {{slot}}", diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json index 8d84b9e341f..daeafa3f647 100644 --- a/app/src/assets/localization/en/protocol_command_text.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -2,10 +2,13 @@ "adapter_in_mod_in_slot": "{{adapter}} on {{module}} in {{slot}}", "adapter_in_slot": "{{adapter}} in {{slot}}", "aspirate": "Aspirating {{volume}} µL from well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", + "aspirate_in_place": "Aspirating {{volume}} µL in place at {{flow_rate}} µL/sec ", "blowout": "Blowing out at well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", + "blowout_in_place": "Blowing out in place at {{flow_rate}} µL/sec", "closing_tc_lid": "Closing Thermocycler lid", "comment": "Comment", "configure_for_volume": "Configure {{pipette}} to aspirate {{volume}} µL", + "configure_nozzle_layout": "Configure {{pipette}} to use {{amount}} nozzles", "confirm_and_resume": "Confirm and resume", "deactivate_hs_shake": "Deactivating shaker", "deactivate_temperature_module": "Deactivating Temperature Module", @@ -16,7 +19,9 @@ "disengaging_magnetic_module": "Disengaging Magnetic Module", "dispense_push_out": "Dispensing {{volume}} µL into well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec and pushing out {{push_out_volume}} µL", "dispense": "Dispensing {{volume}} µL into well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", + "dispense_in_place": "Dispensing {{volume}} µL in place at {{flow_rate}} µL/sec", "drop_tip": "Dropping tip in {{well_name}} of {{labware}}", + "drop_tip_in_place": "Dropping tip in place", "engaging_magnetic_module": "Engaging Magnetic Module", "fixed_trash": "Fixed Trash", "home_gantry": "Homing all gantry, pipette, and plunger axes", @@ -31,13 +36,14 @@ "move_to_coordinates": "Moving to (X: {{x}}, Y: {{y}}, Z: {{z}})", "move_to_slot": "Moving to Slot {{slot_name}}", "move_to_well": "Moving to well {{well_name}} of {{labware}} in {{labware_location}}", + "move_to_addressable_area": "Moving to {{addressable_area}} at {{speed}} mm/s at {{height}} mm high", "notes": "notes", "off_deck": "off deck", "offdeck": "offdeck", "opening_tc_lid": "Opening Thermocycler lid", "pause_on": "Pause on {{robot_name}}", "pause": "Pause", - "pickup_tip": "Picking up tip from {{well_name}} of {{labware}} in {{labware_location}}", + "pickup_tip": "Picking up tip(s) from {{well_range}} of {{labware}} in {{labware_location}}", "prepare_to_aspirate": "Preparing {{pipette}} to aspirate", "return_tip": "Returning tip to {{well_name}} of {{labware}} in {{labware_location}}", "save_position": "Saving position", diff --git a/app/src/assets/localization/en/protocol_info.json b/app/src/assets/localization/en/protocol_info.json index 0fc225e6f65..bbaac1ce9c2 100644 --- a/app/src/assets/localization/en/protocol_info.json +++ b/app/src/assets/localization/en/protocol_info.json @@ -3,7 +3,6 @@ "browse_protocol_library": "Open Protocol Library", "cancel_run": "Cancel Run", "choose_file": "Choose File...", - "choose_protocol_file": "Choose File", "choose_snippet_type": "Choose the Labware Offset Data Python Snippet based on target execution environment.", "continue_proceed_to_calibrate": "Proceed to Calibrate", "continue_verify_calibrations": "Verify pipette and labware calibrations", @@ -86,6 +85,7 @@ "unpin_protocol": "Unpin protocol", "unpinned_protocol": "Unpinned protocol", "update_robot_for_custom_labware": "You have custom labware definitions saved to your app, but this robot needs to be updated before you can use these definitions with Python protocols", + "upload": "Upload", "upload_and_simulate": "Open a protocol to run on {{robot_name}}", "valid_file_types": "Valid file types: Python files (.py) or Protocol Designer files (.json)" } diff --git a/app/src/assets/videos/pipette-wizard-flows/Pipette_Attach_96.webm b/app/src/assets/videos/pipette-wizard-flows/Pipette_Attach_96.webm index 3f219813708..d3486a97089 100644 Binary files a/app/src/assets/videos/pipette-wizard-flows/Pipette_Attach_96.webm and b/app/src/assets/videos/pipette-wizard-flows/Pipette_Attach_96.webm differ diff --git a/app/src/assets/videos/pipette-wizard-flows/Pipette_Detach_96.webm b/app/src/assets/videos/pipette-wizard-flows/Pipette_Detach_96.webm index a8625b78d95..386af577693 100644 Binary files a/app/src/assets/videos/pipette-wizard-flows/Pipette_Detach_96.webm and b/app/src/assets/videos/pipette-wizard-flows/Pipette_Detach_96.webm differ diff --git a/app/src/atoms/StepMeter/__tests__/StepMeter.test.tsx b/app/src/atoms/StepMeter/__tests__/StepMeter.test.tsx index 50aa54e4343..faa688df6f1 100644 --- a/app/src/atoms/StepMeter/__tests__/StepMeter.test.tsx +++ b/app/src/atoms/StepMeter/__tests__/StepMeter.test.tsx @@ -51,4 +51,21 @@ describe('StepMeter', () => { const bar = getByTestId('StepMeter_StepMeterBar') expect(bar).toHaveStyle('width: 100%') }) + + it('should transition with style when progressing forward and no style if progressing backward', () => { + props = { + ...props, + currentStep: 2, + } + const { getByTestId } = render(props) + getByTestId('StepMeter_StepMeterContainer') + const bar = getByTestId('StepMeter_StepMeterBar') + expect(bar).toHaveStyle('transition: width 0.5s ease-in-out;') + + props = { + ...props, + currentStep: 1, + } + expect(bar).not.toHaveStyle('transition: ;') + }) }) diff --git a/app/src/atoms/StepMeter/index.tsx b/app/src/atoms/StepMeter/index.tsx index ddaf52565cf..0d9774bd363 100644 --- a/app/src/atoms/StepMeter/index.tsx +++ b/app/src/atoms/StepMeter/index.tsx @@ -16,13 +16,13 @@ interface StepMeterProps { export const StepMeter = (props: StepMeterProps): JSX.Element => { const { totalSteps, currentStep } = props + const prevPercentComplete = React.useRef(0) const progress = currentStep != null ? currentStep : 0 - const percentComplete = `${ + const percentComplete = // this logic puts a cap at 100% percentComplete which we should never run into currentStep != null && currentStep > totalSteps ? 100 : (progress / totalSteps) * 100 - }%` const StepMeterContainer = css` position: ${POSITION_RELATIVE}; @@ -45,11 +45,15 @@ export const StepMeter = (props: StepMeterProps): JSX.Element => { top: 0; height: 100%; background-color: ${COLORS.blueEnabled}; - width: ${percentComplete}; + width: ${percentComplete}%; transform: translateX(0); - transition: width 0.5s ease-in-out; + transition: ${prevPercentComplete.current <= percentComplete + ? 'width 0.5s ease-in-out' + : ''}; ` + prevPercentComplete.current = percentComplete + return ( diff --git a/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx b/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx index d1d5fe35282..027160db13f 100644 --- a/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx +++ b/app/src/molecules/DeckThumbnail/__tests__/DeckThumbnail.test.tsx @@ -3,7 +3,6 @@ import { when, resetAllWhenMocks } from 'jest-when' import { FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, OT2_ROBOT_TYPE, } from '@opentrons/shared-data' import ot2StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot2_standard.json' @@ -11,7 +10,6 @@ import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_st import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' import { BaseDeck, - EXTENDED_DECK_CONFIG_FIXTURE, partialComponentPropsMatcher, renderWithProviders, } from '@opentrons/components' @@ -24,7 +22,7 @@ import { import { i18n } from '../../../i18n' import { useAttachedModules } from '../../../organisms/Devices/hooks' import { getStandardDeckViewLayerBlockList } from '../utils/getStandardDeckViewLayerBlockList' -import { getDeckConfigFromProtocolCommands } from '../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../resources/deck_configuration/utils' import { getAttachedProtocolModuleMatches } from '../../../organisms/ProtocolSetupModulesAndDeck/utils' import { getProtocolModulesInfo } from '../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { getLabwareRenderInfo } from '../../../organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo' @@ -34,7 +32,6 @@ import { DeckThumbnail } from '../' import type { LabwareDefinition2, - LoadedLabware, ModuleModel, ModuleType, RunTimeCommand, @@ -50,10 +47,6 @@ jest.mock('../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo') jest.mock('../../../organisms/Devices/hooks') jest.mock('../../../organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo') -const mockGetRobotTypeFromLoadedLabware = getRobotTypeFromLoadedLabware as jest.MockedFunction< - typeof getRobotTypeFromLoadedLabware -> - const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction< typeof getDeckDefFromRobotType > @@ -66,8 +59,8 @@ const mockParseLabwareInfoByLiquidId = parseLabwareInfoByLiquidId as jest.Mocked const mockUseAttachedModules = useAttachedModules as jest.MockedFunction< typeof useAttachedModules > -const mockGetDeckConfigFromProtocolCommands = getDeckConfigFromProtocolCommands as jest.MockedFunction< - typeof getDeckConfigFromProtocolCommands +const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction< + typeof getSimplestDeckConfigForProtocolCommands > const mockGetLabwareRenderInfo = getLabwareRenderInfo as jest.MockedFunction< typeof getLabwareRenderInfo @@ -80,9 +73,11 @@ const mockGetAttachedProtocolModuleMatches = getAttachedProtocolModuleMatches as > const mockBaseDeck = BaseDeck as jest.MockedFunction -const protocolAnalysis = simpleAnalysisFileFixture as any +const protocolAnalysis = { + ...simpleAnalysisFileFixture, + robotType: OT2_ROBOT_TYPE, +} as any const commands: RunTimeCommand[] = simpleAnalysisFileFixture.commands as any -const labware: LoadedLabware[] = simpleAnalysisFileFixture.labware as any const MOCK_300_UL_TIPRACK_ID = '300_ul_tiprack_id' const MOCK_MAGNETIC_MODULE_COORDS = [10, 20, 0] const MOCK_SECOND_MAGNETIC_MODULE_COORDS = [100, 200, 0] @@ -112,9 +107,6 @@ const render = (props: React.ComponentProps) => { describe('DeckThumbnail', () => { beforeEach(() => { - when(mockGetRobotTypeFromLoadedLabware) - .calledWith(labware) - .mockReturnValue(OT2_ROBOT_TYPE) when(mockGetDeckDefFromRobotType) .calledWith(OT2_ROBOT_TYPE) .mockReturnValue(ot2StandardDeckDef as any) @@ -127,9 +119,11 @@ describe('DeckThumbnail', () => { mockUseAttachedModules.mockReturnValue( mockFetchModulesSuccessActionPayloadModules ) - when(mockGetDeckConfigFromProtocolCommands) + when(mockGetSimplestDeckConfigForProtocolCommands) .calledWith(commands) - .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE) + .mockReturnValue([]) + // TODO(bh, 2023-11-13): mock the cutout config protocol spec + // .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE) when(mockGetLabwareRenderInfo).mockReturnValue({}) when(mockGetProtocolModulesInfo) .calledWith(protocolAnalysis, ot2StandardDeckDef as any) @@ -188,6 +182,21 @@ describe('DeckThumbnail', () => { getByText('mock BaseDeck') }) + it('returns null when there is no protocolAnalysis or the protocolAnalysis contains an error', () => { + const { queryByText } = render({ + protocolAnalysis: null, + }) + expect(queryByText('mock BaseDeck')).not.toBeInTheDocument() + + render({ + protocolAnalysis: { + ...protocolAnalysis, + errors: 'test error', + }, + }) + expect(queryByText('mock BaseDeck')).not.toBeInTheDocument() + }) + it('renders an OT-3 deck view when the protocol is an OT-3 protocol', () => { // ToDo (kk:11/06/2023) update this test later // const mockLabwareLocations = [ @@ -212,9 +221,6 @@ describe('DeckThumbnail', () => { // nestedLabwareDef: null, // }, // ] - when(mockGetRobotTypeFromLoadedLabware) - .calledWith(labware) - .mockReturnValue(FLEX_ROBOT_TYPE) when(mockGetDeckDefFromRobotType) .calledWith(FLEX_ROBOT_TYPE) .mockReturnValue(ot3StandardDeckDef as any) @@ -224,9 +230,11 @@ describe('DeckThumbnail', () => { mockUseAttachedModules.mockReturnValue( mockFetchModulesSuccessActionPayloadModules ) - when(mockGetDeckConfigFromProtocolCommands) + when(mockGetSimplestDeckConfigForProtocolCommands) .calledWith(commands) - .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE) + .mockReturnValue([]) + // TODO(bh, 2023-11-13): mock the cutout config protocol spec + // .mockReturnValue(EXTENDED_DECK_CONFIG_FIXTURE) when(mockGetLabwareRenderInfo).mockReturnValue({ [MOCK_300_UL_TIPRACK_ID]: { labwareDef: fixture_tiprack_300_ul as LabwareDefinition2, @@ -280,7 +288,6 @@ describe('DeckThumbnail', () => { deckLayerBlocklist: getStandardDeckViewLayerBlockList( FLEX_ROBOT_TYPE ), - deckConfig: EXTENDED_DECK_CONFIG_FIXTURE, labwareLocations: expect.anything(), moduleLocations: expect.anything(), }) diff --git a/app/src/molecules/DeckThumbnail/index.tsx b/app/src/molecules/DeckThumbnail/index.tsx index fed45696e55..8fa85c62604 100644 --- a/app/src/molecules/DeckThumbnail/index.tsx +++ b/app/src/molecules/DeckThumbnail/index.tsx @@ -3,8 +3,8 @@ import map from 'lodash/map' import { BaseDeck } from '@opentrons/components' import { + FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, THERMOCYCLER_MODULE_V1, } from '@opentrons/shared-data' import { @@ -13,7 +13,7 @@ import { } from '@opentrons/api-client' import { getStandardDeckViewLayerBlockList } from './utils/getStandardDeckViewLayerBlockList' -import { getDeckConfigFromProtocolCommands } from '../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import { getLabwareRenderInfo } from '../../organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo' import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { useAttachedModules } from '../../organisms/Devices/hooks' @@ -35,27 +35,20 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element | null { const { protocolAnalysis, showSlotLabels = false, ...styleProps } = props const attachedModules = useAttachedModules() - if (protocolAnalysis == null) return null - - const robotType = getRobotTypeFromLoadedLabware(protocolAnalysis.labware) + if (protocolAnalysis == null || protocolAnalysis.errors.length) return null + const robotType = protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE const deckDef = getDeckDefFromRobotType(robotType) const initialLoadedLabwareByAdapter = parseInitialLoadedLabwareByAdapter( protocolAnalysis.commands ) - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( protocolAnalysis.commands ) const liquids = protocolAnalysis.liquids + const labwareRenderInfo = getLabwareRenderInfo(protocolAnalysis, deckDef) + const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef) - const labwareRenderInfo = - protocolAnalysis != null - ? getLabwareRenderInfo(protocolAnalysis, deckDef) - : {} - const protocolModulesInfo = - protocolAnalysis != null - ? getProtocolModulesInfo(protocolAnalysis, deckDef) - : [] const attachedProtocolModuleMatches = getAttachedProtocolModuleMatches( attachedModules, protocolModulesInfo @@ -93,7 +86,7 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element | null { const labwareLocations = map( labwareRenderInfo, - ({ labwareDef, displayName, slotName }, labwareId) => { + ({ labwareDef, slotName }, labwareId) => { const labwareInAdapter = initialLoadedLabwareByAdapter[labwareId] // only rendering the labware on top most layer so // either the adapter or the labware are rendered but not both diff --git a/app/src/molecules/ReleaseNotes/index.tsx b/app/src/molecules/ReleaseNotes/index.tsx index 3d54e195783..537763bdc94 100644 --- a/app/src/molecules/ReleaseNotes/index.tsx +++ b/app/src/molecules/ReleaseNotes/index.tsx @@ -26,7 +26,6 @@ const DEFAULT_RELEASE_NOTES = 'We recommend upgrading to the latest version.' export function ReleaseNotes(props: ReleaseNotesProps): JSX.Element { const { source } = props - console.log(DEFAULT_RELEASE_NOTES) return (
{source != null ? ( diff --git a/app/src/molecules/UploadInput/__tests__/UploadInput.test.tsx b/app/src/molecules/UploadInput/__tests__/UploadInput.test.tsx index 945dfe756be..9da307296b0 100644 --- a/app/src/molecules/UploadInput/__tests__/UploadInput.test.tsx +++ b/app/src/molecules/UploadInput/__tests__/UploadInput.test.tsx @@ -30,12 +30,12 @@ describe('UploadInput', () => { it('renders correct contents for empty state', () => { const { getByRole } = render() - expect(getByRole('button', { name: 'Choose File' })).toBeTruthy() + expect(getByRole('button', { name: 'Upload' })).toBeTruthy() }) it('opens file select on button click', () => { const { getByRole, getByTestId } = render() - const button = getByRole('button', { name: 'Choose File' }) + const button = getByRole('button', { name: 'Upload' }) const input = getByTestId('file_input') input.click = jest.fn() fireEvent.click(button) diff --git a/app/src/molecules/UploadInput/index.tsx b/app/src/molecules/UploadInput/index.tsx index 81c52e244ec..d3fe7571d93 100644 --- a/app/src/molecules/UploadInput/index.tsx +++ b/app/src/molecules/UploadInput/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { css } from 'styled-components' +import styled, { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { Icon, @@ -16,7 +16,7 @@ import { } from '@opentrons/components' import { StyledText } from '../../atoms/text' -const DROP_ZONE_STYLES = css` +const StyledLabel = styled.label` display: flex; cursor: pointer; flex-direction: ${DIRECTION_COLUMN}; @@ -39,7 +39,7 @@ const DRAG_OVER_STYLES = css` border: 2px dashed ${COLORS.blueEnabled}; ` -const INPUT_STYLES = css` +const StyledInput = styled.input` position: fixed; clip: rect(1px 1px 1px 1px); ` @@ -61,8 +61,7 @@ export function UploadInput(props: UploadInputProps): JSX.Element | null { const handleDrop: React.DragEventHandler = e => { e.preventDefault() e.stopPropagation() - const { files = [] } = 'dataTransfer' in e ? e.dataTransfer : {} - props.onUpload(files[0]) + Array.from(e.dataTransfer.files).forEach(f => props.onUpload(f)) setIsFileOverDropZone(false) } const handleDragEnter: React.DragEventHandler = e => { @@ -85,17 +84,10 @@ export function UploadInput(props: UploadInputProps): JSX.Element | null { } const onChange: React.ChangeEventHandler = event => { - const { files = [] } = event.target ?? {} - files?.[0] != null && props.onUpload(files?.[0]) + ;[...(event.target.files ?? [])].forEach(f => props.onUpload(f)) if ('value' in event.currentTarget) event.currentTarget.value = '' } - const dropZoneStyles = isFileOverDropZone - ? css` - ${DROP_ZONE_STYLES} ${DRAG_OVER_STYLES} - ` - : DROP_ZONE_STYLES - return ( - {t('choose_protocol_file')} + {t('upload')} - + ) } diff --git a/app/src/molecules/WizardRequiredEquipmentList/index.tsx b/app/src/molecules/WizardRequiredEquipmentList/index.tsx index 35b87750b4b..4d295203b5c 100644 --- a/app/src/molecules/WizardRequiredEquipmentList/index.tsx +++ b/app/src/molecules/WizardRequiredEquipmentList/index.tsx @@ -83,12 +83,11 @@ export function WizardRequiredEquipmentList( > {t('you_will_need')} - {equipmentList.length > 1 ? : null} - {equipmentList.map((requiredEquipmentProps, index) => ( + + {equipmentList.map(requiredEquipmentProps => ( ))} {footer != null ? ( diff --git a/app/src/molecules/modals/ErrorModal.tsx b/app/src/molecules/modals/ErrorModal.tsx index b31952b9f2a..3951eb132a9 100644 --- a/app/src/molecules/modals/ErrorModal.tsx +++ b/app/src/molecules/modals/ErrorModal.tsx @@ -11,7 +11,7 @@ interface Props { description: string close?: () => unknown closeUrl?: string - error: { message?: string; [key: string]: unknown } + error: { message?: string; [key: string]: unknown } | null } const DEFAULT_HEADING = 'Unexpected Error' @@ -37,7 +37,7 @@ export function ErrorModal(props: Props): JSX.Element {

- {error.message ?? AN_UNKNOWN_ERROR_OCCURRED} + {error?.message ?? AN_UNKNOWN_ERROR_OCCURRED}

{description}

diff --git a/app/src/organisms/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx b/app/src/organisms/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx index a5efa4a80e7..7bf3f23675f 100644 --- a/app/src/organisms/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx +++ b/app/src/organisms/AddCustomLabwareSlideout/__tests__/AddCustomLabwareSlideout.test.tsx @@ -46,7 +46,7 @@ describe('AddCustomLabwareSlideout', () => { const [{ getByText, getByRole }] = render(props) getByText('Import a Custom Labware Definition') getByText('Or choose a file from your computer to upload.') - const btn = getByRole('button', { name: 'Choose File' }) + const btn = getByRole('button', { name: 'Upload' }) fireEvent.click(btn) expect(mockTrackEvent).toHaveBeenCalledWith({ name: ANALYTICS_ADD_CUSTOM_LABWARE, diff --git a/app/src/organisms/ApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts b/app/src/organisms/ApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts index b297775880f..2555c5e8441 100644 --- a/app/src/organisms/ApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts +++ b/app/src/organisms/ApplyHistoricOffsets/hooks/getLabwareLocationCombos.ts @@ -59,7 +59,12 @@ export function getLabwareLocationCombos( }) } else { return appendLocationComboIfUniq(acc, { - location: command.params.location, + location: { + slotName: + 'addressableAreaName' in command.params.location + ? command.params.location.addressableAreaName + : command.params.location.slotName, + }, definitionUri, labwareId: command.result.labwareId, }) @@ -107,7 +112,12 @@ export function getLabwareLocationCombos( }) } else { return appendLocationComboIfUniq(acc, { - location: command.params.newLocation, + location: { + slotName: + 'addressableAreaName' in command.params.newLocation + ? command.params.newLocation.addressableAreaName + : command.params.newLocation.slotName, + }, definitionUri: labwareEntity.definitionUri, labwareId: command.params.labwareId, }) @@ -191,7 +201,10 @@ function resolveAdapterLocation( } else { adapterOffsetLocation = { definitionUri: labwareDefUri, - slotName: labwareEntity.location.slotName, + slotName: + 'addressableAreaName' in labwareEntity.location + ? labwareEntity.location.addressableAreaName + : labwareEntity.location.slotName, } } return { diff --git a/app/src/organisms/CalibrateDeck/index.tsx b/app/src/organisms/CalibrateDeck/index.tsx index 5beac8275f3..87d0a65f432 100644 --- a/app/src/organisms/CalibrateDeck/index.tsx +++ b/app/src/organisms/CalibrateDeck/index.tsx @@ -175,6 +175,7 @@ export function CalibrateDeck( supportedCommands={supportedCommands} defaultTipracks={instrument?.defaultTipracks} calInvalidationHandler={offsetInvalidationHandler} + allowChangeTipRack /> )} diff --git a/app/src/organisms/CalibrateTipLength/index.tsx b/app/src/organisms/CalibrateTipLength/index.tsx index bc63d34f134..21016afa1de 100644 --- a/app/src/organisms/CalibrateTipLength/index.tsx +++ b/app/src/organisms/CalibrateTipLength/index.tsx @@ -71,8 +71,10 @@ export function CalibrateTipLength( dispatchRequests, isJogging, offsetInvalidationHandler, + allowChangeTipRack = false, } = props - const { currentStep, instrument, labware } = session?.details ?? {} + const { currentStep, instrument, labware, supportedCommands } = + session?.details ?? {} const queryClient = useQueryClient() const host = useHost() @@ -171,7 +173,9 @@ export function CalibrateTipLength( calBlock={calBlock} currentStep={currentStep} sessionType={session.sessionType} + supportedCommands={supportedCommands} calInvalidationHandler={offsetInvalidationHandler} + allowChangeTipRack={allowChangeTipRack} /> )} diff --git a/app/src/organisms/CalibrateTipLength/types.ts b/app/src/organisms/CalibrateTipLength/types.ts index 48f685343e1..bcb791c5f5d 100644 --- a/app/src/organisms/CalibrateTipLength/types.ts +++ b/app/src/organisms/CalibrateTipLength/types.ts @@ -7,5 +7,6 @@ export interface CalibrateTipLengthParentProps { dispatchRequests: DispatchRequestsType showSpinner: boolean isJogging: boolean + allowChangeTipRack?: boolean offsetInvalidationHandler?: () => void } diff --git a/app/src/organisms/CalibrationPanels/DeckSetup.tsx b/app/src/organisms/CalibrationPanels/DeckSetup.tsx index 3b3b208b381..dd19e6254f2 100644 --- a/app/src/organisms/CalibrationPanels/DeckSetup.tsx +++ b/app/src/organisms/CalibrationPanels/DeckSetup.tsx @@ -98,6 +98,7 @@ export function DeckSetup(props: CalibrationPanelProps): JSX.Element { 'fixedTrash', ]} deckDef={deckDef} + showDeckLayers viewBox={`-46 -10 ${488} ${390}`} // TODO: put these in variables > {({ deckSlotsById }) => diff --git a/app/src/organisms/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx b/app/src/organisms/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx index bccf4a9202b..8b0cb221597 100644 --- a/app/src/organisms/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx +++ b/app/src/organisms/CalibrationPanels/Introduction/__tests__/Introduction.test.tsx @@ -52,9 +52,10 @@ describe('Introduction', () => { getByRole('link', { name: 'Need help?' }) expect(queryByRole('button', { name: 'Change tip rack' })).toBe(null) }) - it('renders change tip rack button if deck calibration', () => { + it('renders change tip rack button if allowChangeTipRack', () => { const { getByRole, getByText, queryByRole } = render({ sessionType: Sessions.SESSION_TYPE_DECK_CALIBRATION, + allowChangeTipRack: true, })[0] const button = getByRole('button', { name: 'Change tip rack' }) button.click() diff --git a/app/src/organisms/CalibrationPanels/Introduction/index.tsx b/app/src/organisms/CalibrationPanels/Introduction/index.tsx index 9a70d1ea0ad..5bdc25ce51c 100644 --- a/app/src/organisms/CalibrationPanels/Introduction/index.tsx +++ b/app/src/organisms/CalibrationPanels/Introduction/index.tsx @@ -35,6 +35,7 @@ export function Introduction(props: CalibrationPanelProps): JSX.Element { instruments, supportedCommands, calInvalidationHandler, + allowChangeTipRack = false, } = props const { t } = useTranslation('robot_calibration') @@ -167,7 +168,7 @@ export function Introduction(props: CalibrationPanelProps): JSX.Element { > - {sessionType === Sessions.SESSION_TYPE_DECK_CALIBRATION ? ( + {allowChangeTipRack ? ( setShowChooseTipRack(true)}> {t('change_tip_rack')} diff --git a/app/src/organisms/CalibrationPanels/types.ts b/app/src/organisms/CalibrationPanels/types.ts index 16d9f9d60fd..cc8700aa8be 100644 --- a/app/src/organisms/CalibrationPanels/types.ts +++ b/app/src/organisms/CalibrationPanels/types.ts @@ -32,4 +32,5 @@ export interface CalibrationPanelProps { supportedCommands?: SessionCommandString[] | null defaultTipracks?: LabwareDefinition2[] | null calInvalidationHandler?: () => void + allowChangeTipRack?: boolean } diff --git a/app/src/organisms/ChooseProtocolSlideout/index.tsx b/app/src/organisms/ChooseProtocolSlideout/index.tsx index 67a4148d54e..ab3b201318b 100644 --- a/app/src/organisms/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseProtocolSlideout/index.tsx @@ -36,7 +36,6 @@ import { useCreateRunFromProtocol } from '../ChooseRobotToRunProtocolSlideout/us import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' -import { ProtocolAnalysisOutput } from '@opentrons/shared-data' import type { Robot } from '../../redux/discovery/types' import type { StoredProtocolData } from '../../redux/protocol-storage' import type { State } from '../../redux/types' @@ -163,7 +162,6 @@ export function ChooseProtocolSlideoutComponent( }} robotName={robot.name} {...{ selectedProtocol, runCreationError, runCreationErrorCode }} - protocolAnalysis={selectedProtocol?.mostRecentAnalysis} /> ) : null} @@ -182,7 +180,6 @@ interface StoredProtocolListProps { runCreationError: string | null runCreationErrorCode: number | null robotName: string - protocolAnalysis?: ProtocolAnalysisOutput | null } function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { @@ -192,7 +189,6 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { runCreationError, runCreationErrorCode, robotName, - protocolAnalysis, } = props const { t } = useTranslation(['device_details', 'shared']) const storedProtocols = useSelector((state: State) => @@ -227,8 +223,10 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { height="4.25rem" width="4.75rem" > - {protocolAnalysis != null ? ( - + {storedProtocol.mostRecentAnalysis != null ? ( + ) : null} { const { t } = useTranslation('protocol_command_text') - const { labwareId, wellName } = command.params + const labwareId = + 'labwareId' in command.params ? command.params.labwareId : '' + const wellName = 'wellName' in command.params ? command.params.wellName : '' + const allPreviousCommands = robotSideAnalysis.commands.slice( 0, robotSideAnalysis.commands.findIndex(c => c.id === command.id) @@ -95,13 +92,6 @@ export const PipettingCommandText = ({ flow_rate: flowRate, }) } - case 'moveToWell': { - return t('move_to_well', { - well_name: wellName, - labware: getLabwareName(robotSideAnalysis, labwareId), - labware_location: displayLocation, - }) - } case 'dropTip': { const loadedLabware = getLoadedLabware(robotSideAnalysis, labwareId) const labwareDefinitions = getLabwareDefinitionsFromCommands( @@ -122,12 +112,39 @@ export const PipettingCommandText = ({ }) } case 'pickUpTip': { + const pipetteId = command.params.pipetteId + const pipetteName: + | PipetteName + | undefined = robotSideAnalysis.pipettes.find( + pip => pip.id === pipetteId + )?.pipetteName + return t('pickup_tip', { - well_name: wellName, + well_range: getWellRange( + pipetteId, + allPreviousCommands, + wellName, + pipetteName + ), labware: getLabwareName(robotSideAnalysis, labwareId), labware_location: displayLocation, }) } + case 'dropTipInPlace': { + return t('drop_tip_in_place') + } + case 'dispenseInPlace': { + const { volume, flowRate } = command.params + return t('dispense_in_place', { volume: volume, flow_rate: flowRate }) + } + case 'blowOutInPlace': { + const { flowRate } = command.params + return t('blowout_in_place', { flow_rate: flowRate }) + } + case 'aspirateInPlace': { + const { flowRate, volume } = command.params + return t('aspirate_in_place', { volume, flow_rate: flowRate }) + } default: { console.warn( 'PipettingCommandText encountered a command with an unrecognized commandType: ', diff --git a/app/src/organisms/CommandText/__tests__/CommandText.test.tsx b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx index 28ef08f940f..8a481d8b60b 100644 --- a/app/src/organisms/CommandText/__tests__/CommandText.test.tsx +++ b/app/src/organisms/CommandText/__tests__/CommandText.test.tsx @@ -1,7 +1,12 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' import { + AspirateInPlaceRunTimeCommand, + BlowoutInPlaceRunTimeCommand, + DispenseInPlaceRunTimeCommand, + DropTipInPlaceRunTimeCommand, FLEX_ROBOT_TYPE, + MoveToAddressableAreaRunTimeCommand, PrepareToAspirateRunTimeCommand, } from '@opentrons/shared-data' import { i18n } from '../../../i18n' @@ -85,6 +90,26 @@ describe('CommandText', () => { ) } }) + it('renders correct text for dispenseInPlace', () => { + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText('Dispensing 50 µL in place at 300 µL/sec') + }) it('renders correct text for blowout', () => { const dispenseCommand = mockRobotSideAnalysis.commands.find( c => c.commandType === 'dispense' @@ -108,6 +133,45 @@ describe('CommandText', () => { ) } }) + it('renders correct text for blowOutInPlace', () => { + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText('Blowing out in place at 300 µL/sec') + }) + it('renders correct text for aspirateInPlace', () => { + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText('Aspirating 10 µL in place at 300 µL/sec') + }) it('renders correct text for moveToWell', () => { const dispenseCommand = mockRobotSideAnalysis.commands.find( c => c.commandType === 'aspirate' @@ -129,6 +193,27 @@ describe('CommandText', () => { getByText('Moving to well A1 of NEST 1 Well Reservoir 195 mL in Slot 5') } }) + it('renders correct text for moveToAddressableArea', () => { + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText('Moving to D3 at 200 mm/s at 100 mm high') + }) it('renders correct text for configureForVolume', () => { const command = { commandType: 'configureForVolume', @@ -204,6 +289,24 @@ describe('CommandText', () => { )[0] getByText('Returning tip to A1 of Opentrons 96 Tip Rack 300 µL in Slot 9') }) + it('renders correct text for dropTipInPlace', () => { + const { getByText } = renderWithProviders( + , + { i18nInstance: i18n } + )[0] + getByText('Dropping tip in place') + }) it('renders correct text for pickUpTip', () => { const command = mockRobotSideAnalysis.commands.find( c => c.commandType === 'pickUpTip' @@ -219,7 +322,7 @@ describe('CommandText', () => { { i18nInstance: i18n } )[0] getByText( - 'Picking up tip from A1 of Opentrons 96 Tip Rack 300 µL in Slot 9' + 'Picking up tip(s) from A1 of Opentrons 96 Tip Rack 300 µL in Slot 9' ) } }) diff --git a/app/src/organisms/CommandText/index.tsx b/app/src/organisms/CommandText/index.tsx index 67e76526ab1..f39b43299bd 100644 --- a/app/src/organisms/CommandText/index.tsx +++ b/app/src/organisms/CommandText/index.tsx @@ -3,6 +3,11 @@ import { useTranslation } from 'react-i18next' import { Flex, DIRECTION_COLUMN, SPACING } from '@opentrons/components' import { getPipetteNameSpecs, RunTimeCommand } from '@opentrons/shared-data' +import { + getLabwareName, + getLabwareDisplayLocation, + getFinalLabwareLocation, +} from './utils' import { StyledText } from '../../atoms/text' import { LoadCommandText } from './LoadCommandText' import { PipettingCommandText } from './PipettingCommandText' @@ -49,10 +54,13 @@ export function CommandText(props: Props): JSX.Element | null { switch (command.commandType) { case 'aspirate': + case 'aspirateInPlace': case 'dispense': + case 'dispenseInPlace': case 'blowout': - case 'moveToWell': + case 'blowOutInPlace': case 'dropTip': + case 'dropTipInPlace': case 'pickUpTip': { return ( @@ -138,6 +146,31 @@ export function CommandText(props: Props): JSX.Element | null { ) } + case 'moveToWell': { + const { wellName, labwareId } = command.params + const allPreviousCommands = robotSideAnalysis.commands.slice( + 0, + robotSideAnalysis.commands.findIndex(c => c.id === command.id) + ) + const labwareLocation = getFinalLabwareLocation( + labwareId, + allPreviousCommands + ) + const displayLocation = + labwareLocation != null + ? getLabwareDisplayLocation( + robotSideAnalysis, + labwareLocation, + t, + robotType + ) + : '' + return t('move_to_well', { + well_name: wellName, + labware: getLabwareName(robotSideAnalysis, labwareId), + labware_location: displayLocation, + }) + } case 'moveLabware': { return ( @@ -165,6 +198,25 @@ export function CommandText(props: Props): JSX.Element | null { ) } + case 'configureNozzleLayout': { + const { configuration_params, pipetteId } = command.params + const pipetteName = robotSideAnalysis.pipettes.find( + pip => pip.id === pipetteId + )?.pipetteName + + // TODO (sb, 11/9/23): Add support for other configurations when needed + return ( + + {t('configure_nozzle_layout', { + amount: configuration_params.style === 'COLUMN' ? '8' : 'all', + pipette: + pipetteName != null + ? getPipetteNameSpecs(pipetteName)?.displayName + : '', + })} + + ) + } case 'prepareToAspirate': { const { pipetteId } = command.params const pipetteName = robotSideAnalysis.pipettes.find( @@ -182,6 +234,18 @@ export function CommandText(props: Props): JSX.Element | null { ) } + case 'moveToAddressableArea': { + const { addressableAreaName, speed, minimumZHeight } = command.params + return ( + + {t('move_to_addressable_area', { + addressable_area: addressableAreaName, + speed: speed, + height: minimumZHeight, + })} + + ) + } case 'touchTip': case 'home': case 'savePosition': diff --git a/app/src/organisms/CommandText/utils/getWellRange.ts b/app/src/organisms/CommandText/utils/getWellRange.ts new file mode 100644 index 00000000000..8baa6c0b709 --- /dev/null +++ b/app/src/organisms/CommandText/utils/getWellRange.ts @@ -0,0 +1,46 @@ +import { + getPipetteNameSpecs, + PipetteName, + RunTimeCommand, +} from '@opentrons/shared-data' + +/** + * @param pipetteName name of pipette being used + * @param commands list of commands to search within + * @param wellName the target well for pickup tip + * @returns WellRange string of wells pipette will pickup tips from + */ +export function getWellRange( + pipetteId: string, + commands: RunTimeCommand[], + wellName: string, + pipetteName?: PipetteName +): string { + const pipetteChannels = pipetteName + ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 + : 1 + let usedChannels = pipetteChannels + if (pipetteChannels === 96) { + for (const c of commands.reverse()) { + if ( + c.commandType === 'configureNozzleLayout' && + c.params?.pipetteId === pipetteId + ) { + // TODO(sb, 11/9/23): add support for quadrant and row configurations when needed + if (c.params.configuration_params.style === 'SINGLE') { + usedChannels = 1 + } else if (c.params.configuration_params.style === 'COLUMN') { + usedChannels = 8 + } + break + } + } + } + if (usedChannels === 96) { + return 'A1 - H12' + } else if (usedChannels === 8) { + const column = wellName.substr(1) + return `A${column} - H${column}` + } + return wellName +} diff --git a/app/src/organisms/CommandText/utils/index.ts b/app/src/organisms/CommandText/utils/index.ts index 0b7a5c24124..5435a292d11 100644 --- a/app/src/organisms/CommandText/utils/index.ts +++ b/app/src/organisms/CommandText/utils/index.ts @@ -5,3 +5,4 @@ export * from './getModuleDisplayLocation' export * from './getLiquidDisplayName' export * from './getLabwareDisplayLocation' export * from './getFinalLabwareLocation' +export * from './getWellRange' diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx index 6b618c0fc1e..cc5ddd4f4e7 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.stories.tsx @@ -25,7 +25,7 @@ const Template: Story> = args => ( export const Default = Template.bind({}) Default.args = { - fixtureLocation: 'D3', + fixtureLocation: 'cutoutD3', setShowAddFixtureModal: () => {}, isOnDevice: true, } diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx index a1cb43abb46..a33f55db2a2 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx @@ -14,12 +14,18 @@ import { SPACING, TYPOGRAPHY, } from '@opentrons/components' -import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' import { + useDeckConfigurationQuery, + useUpdateDeckConfigurationMutation, +} from '@opentrons/react-api-client' +import { + getCutoutDisplayName, getFixtureDisplayName, - STAGING_AREA_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + STAGING_AREA_CUTOUTS, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_CUTOUT, + WASTE_CHUTE_FIXTURES, } from '@opentrons/shared-data' import { StyledText } from '../../atoms/text' @@ -29,24 +35,24 @@ import { Modal } from '../../molecules/Modal' import { LegacyModal } from '../../molecules/LegacyModal' import type { - Cutout, + CutoutConfig, + CutoutId, + CutoutFixtureId, DeckConfiguration, - Fixture, - FixtureLoadName, } from '@opentrons/shared-data' import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' import type { LegacyModalProps } from '../../molecules/LegacyModal' interface AddFixtureModalProps { - fixtureLocation: Cutout + cutoutId: CutoutId setShowAddFixtureModal: (showAddFixtureModal: boolean) => void - setCurrentDeckConfig?: React.Dispatch> - providedFixtureOptions?: FixtureLoadName[] + setCurrentDeckConfig?: React.Dispatch> + providedFixtureOptions?: CutoutFixtureId[] isOnDevice?: boolean } export function AddFixtureModal({ - fixtureLocation, + cutoutId, setShowAddFixtureModal, setCurrentDeckConfig, providedFixtureOptions, @@ -54,41 +60,42 @@ export function AddFixtureModal({ }: AddFixtureModalProps): JSX.Element { const { t } = useTranslation('device_details') const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() + const deckConfig = useDeckConfigurationQuery()?.data ?? [] const modalHeader: ModalHeaderBaseProps = { - title: t('add_to_slot', { slotName: fixtureLocation }), + title: t('add_to_slot', { + slotName: getCutoutDisplayName(cutoutId), + }), hasExitIcon: true, onClick: () => setShowAddFixtureModal(false), } const modalProps: LegacyModalProps = { - title: t('add_to_slot', { slotName: fixtureLocation }), + title: t('add_to_slot', { + slotName: getCutoutDisplayName(cutoutId), + }), onClose: () => setShowAddFixtureModal(false), closeOnOutsideClick: true, childrenPadding: SPACING.spacing24, width: '23.125rem', } - const availableFixtures: FixtureLoadName[] = [TRASH_BIN_LOAD_NAME] - if ( - fixtureLocation === 'A3' || - fixtureLocation === 'B3' || - fixtureLocation === 'C3' - ) { - availableFixtures.push(STAGING_AREA_LOAD_NAME) + const availableFixtures: CutoutFixtureId[] = [TRASH_BIN_ADAPTER_FIXTURE] + if (STAGING_AREA_CUTOUTS.includes(cutoutId)) { + availableFixtures.push(STAGING_AREA_RIGHT_SLOT_FIXTURE) } - if (fixtureLocation === 'D3') { - availableFixtures.push(STAGING_AREA_LOAD_NAME, WASTE_CHUTE_LOAD_NAME) + if (cutoutId === WASTE_CHUTE_CUTOUT) { + availableFixtures.push(...WASTE_CHUTE_FIXTURES) } // For Touchscreen app - const handleTapAdd = (fixtureLoadName: FixtureLoadName): void => { + const handleTapAdd = (requiredFixtureId: CutoutFixtureId): void => { if (setCurrentDeckConfig != null) setCurrentDeckConfig( (prevDeckConfig: DeckConfiguration): DeckConfiguration => - prevDeckConfig.map((fixture: Fixture) => - fixture.fixtureLocation === fixtureLocation - ? { ...fixture, loadName: fixtureLoadName } + prevDeckConfig.map((fixture: CutoutConfig) => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: requiredFixtureId } : fixture ) ) @@ -99,11 +106,14 @@ export function AddFixtureModal({ // For Desktop app const fixtureOptions = providedFixtureOptions ?? availableFixtures - const handleClickAdd = (fixtureLoadName: FixtureLoadName): void => { - updateDeckConfiguration({ - fixtureLocation, - loadName: fixtureLoadName, - }) + const handleClickAdd = (requiredFixtureId: CutoutFixtureId): void => { + const newDeckConfig = deckConfig.map(fixture => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: requiredFixtureId } + : fixture + ) + + updateDeckConfiguration(newDeckConfig) setShowAddFixtureModal(false) } @@ -117,10 +127,10 @@ export function AddFixtureModal({ {t('add_to_slot_description')} - {fixtureOptions.map(fixture => ( - + {fixtureOptions.map(cutoutFixtureId => ( + @@ -161,18 +171,18 @@ export function AddFixtureModal({ } interface AddFixtureButtonProps { - fixtureLoadName: FixtureLoadName - handleClickAdd: (fixtureLoadName: FixtureLoadName) => void + cutoutFixtureId: CutoutFixtureId + handleClickAdd: (cutoutFixtureId: CutoutFixtureId) => void } function AddFixtureButton({ - fixtureLoadName, + cutoutFixtureId, handleClickAdd, }: AddFixtureButtonProps): JSX.Element { const { t } = useTranslation('device_details') return ( handleClickAdd(fixtureLoadName)} + onClick={() => handleClickAdd(cutoutFixtureId)} display="flex" justifyContent={JUSTIFY_SPACE_BETWEEN} flexDirection={DIRECTION_ROW} @@ -181,7 +191,7 @@ function AddFixtureButton({ css={FIXTURE_BUTTON_STYLE} > - {getFixtureDisplayName(fixtureLoadName)} + {getFixtureDisplayName(cutoutFixtureId)} {t('add')} diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx index e497050c353..e31de9bb461 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/AddFixtureModal.test.tsx @@ -1,12 +1,17 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' -import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' -import { TRASH_BIN_LOAD_NAME } from '@opentrons/shared-data' +import { + useDeckConfigurationQuery, + useUpdateDeckConfigurationMutation, +} from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import { AddFixtureModal } from '../AddFixtureModal' +import type { UseQueryResult } from 'react-query' +import type { DeckConfiguration } from '@opentrons/shared-data' + jest.mock('@opentrons/react-api-client') const mockSetShowAddFixtureModal = jest.fn() const mockUpdateDeckConfiguration = jest.fn() @@ -15,6 +20,9 @@ const mockSetCurrentDeckConfig = jest.fn() const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction< typeof useUpdateDeckConfigurationMutation > +const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction< + typeof useDeckConfigurationQuery +> const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -27,7 +35,7 @@ describe('Touchscreen AddFixtureModal', () => { beforeEach(() => { props = { - fixtureLocation: 'D3', + cutoutId: 'cutoutD3', setShowAddFixtureModal: mockSetShowAddFixtureModal, setCurrentDeckConfig: mockSetCurrentDeckConfig, isOnDevice: true, @@ -35,6 +43,9 @@ describe('Touchscreen AddFixtureModal', () => { mockUseUpdateDeckConfigurationMutation.mockReturnValue({ updateDeckConfiguration: mockUpdateDeckConfiguration, } as any) + mockUseDeckConfigurationQuery.mockReturnValue(({ + data: [], + } as unknown) as UseQueryResult) }) it('should render text and buttons', () => { @@ -43,10 +54,10 @@ describe('Touchscreen AddFixtureModal', () => { getByText( 'Choose a fixture below to add to your deck configuration. It will be referenced during protocol analysis.' ) - getByText('Staging Area Slot') - getByText('Trash Bin') - getByText('Waste Chute') - expect(getAllByText('Add').length).toBe(3) + getByText('Staging area slot') + getByText('Trash bin') + getByText('Waste chute only') + expect(getAllByText('Add').length).toBe(6) }) it('should a mock function when tapping app button', () => { @@ -61,7 +72,7 @@ describe('Desktop AddFixtureModal', () => { beforeEach(() => { props = { - fixtureLocation: 'D3', + cutoutId: 'cutoutD3', setShowAddFixtureModal: mockSetShowAddFixtureModal, } mockUseUpdateDeckConfigurationMutation.mockReturnValue({ @@ -79,42 +90,39 @@ describe('Desktop AddFixtureModal', () => { getByText( 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' ) - getByText('Staging Area Slot') - getByText('Trash Bin') - getByText('Waste Chute') - expect(getAllByRole('button', { name: 'Add' }).length).toBe(3) + getByText('Staging area slot') + getByText('Trash bin') + getByText('Waste chute only') + expect(getAllByRole('button', { name: 'Add' }).length).toBe(6) }) it('should render text and buttons slot A1', () => { - props = { ...props, fixtureLocation: 'A1' } + props = { ...props, cutoutId: 'cutoutA1' } const [{ getByText, getByRole }] = render(props) getByText('Add to slot A1') getByText( 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' ) - getByText('Trash Bin') + getByText('Trash bin') getByRole('button', { name: 'Add' }) }) it('should render text and buttons slot B3', () => { - props = { ...props, fixtureLocation: 'B3' } + props = { ...props, cutoutId: 'cutoutB3' } const [{ getByText, getAllByRole }] = render(props) getByText('Add to slot B3') getByText( 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' ) - getByText('Staging Area Slot') - getByText('Trash Bin') + getByText('Staging area slot') + getByText('Trash bin') expect(getAllByRole('button', { name: 'Add' }).length).toBe(2) }) it('should call a mock function when clicking add button', () => { - props = { ...props, fixtureLocation: 'A1' } + props = { ...props, cutoutId: 'cutoutA1' } const [{ getByRole }] = render(props) getByRole('button', { name: 'Add' }).click() - expect(mockUpdateDeckConfiguration).toHaveBeenCalledWith({ - fixtureLocation: 'A1', - loadName: TRASH_BIN_LOAD_NAME, - }) + expect(mockUpdateDeckConfiguration).toHaveBeenCalled() }) }) diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx index 162db0ab1fe..77101c421ce 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx @@ -1,14 +1,24 @@ import * as React from 'react' -import { DeckConfigurator, renderWithProviders } from '@opentrons/components' +import { when, resetAllWhenMocks } from 'jest-when' + +import { + DeckConfigurator, + partialComponentPropsMatcher, + renderWithProviders, +} from '@opentrons/components' import { + useCurrentMaintenanceRun, useDeckConfigurationQuery, useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' + import { i18n } from '../../../i18n' import { useRunStatuses } from '../../Devices/hooks' import { DeckFixtureSetupInstructionsModal } from '../DeckFixtureSetupInstructionsModal' import { DeviceDetailsDeckConfiguration } from '../' +import type { MaintenanceRun } from '@opentrons/api-client' + jest.mock('@opentrons/components/src/hardware-sim/DeckConfigurator/index') jest.mock('@opentrons/react-api-client') jest.mock('../DeckFixtureSetupInstructionsModal') @@ -22,6 +32,9 @@ const RUN_STATUSES = { isRunTerminal: false, isRunIdle: false, } +const mockCurrnetMaintenanceRun = { + data: { id: 'mockMaintenanceRunId' }, +} as MaintenanceRun const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction< typeof useDeckConfigurationQuery @@ -38,6 +51,9 @@ const mockDeckConfigurator = DeckConfigurator as jest.MockedFunction< const mockUseRunStatuses = useRunStatuses as jest.MockedFunction< typeof useRunStatuses > +const mockUseCurrentMaintenanceRun = useCurrentMaintenanceRun as jest.MockedFunction< + typeof useCurrentMaintenanceRun +> const render = ( props: React.ComponentProps @@ -61,8 +77,15 @@ describe('DeviceDetailsDeckConfiguration', () => { mockDeckFixtureSetupInstructionsModal.mockReturnValue(

mock DeckFixtureSetupInstructionsModal
) - mockDeckConfigurator.mockReturnValue(
mock DeckConfigurator
) + when(mockDeckConfigurator).mockReturnValue(
mock DeckConfigurator
) mockUseRunStatuses.mockReturnValue(RUN_STATUSES) + mockUseCurrentMaintenanceRun.mockReturnValue({ + data: {}, + } as any) + }) + + afterEach(() => { + resetAllWhenMocks() }) it('should render text and button', () => { @@ -83,9 +106,31 @@ describe('DeviceDetailsDeckConfiguration', () => { it('should render banner and make deck configurator disabled when running', () => { RUN_STATUSES.isRunRunning = true mockUseRunStatuses.mockReturnValue(RUN_STATUSES) - const [{ getByText, queryAllByRole }] = render(props) + when(mockDeckConfigurator) + .calledWith(partialComponentPropsMatcher({ readOnly: true })) + .mockReturnValue(
disabled mock DeckConfigurator
) + const [{ getByText }] = render(props) getByText('Deck configuration is not available when run is in progress') - // Note (kk:10/27/2023) detects Setup Instructions buttons - expect(queryAllByRole('button').length).toBe(1) + getByText('disabled mock DeckConfigurator') + }) + + it('should render banner and make deck configurator disabled when a maintenance run exists', () => { + mockUseCurrentMaintenanceRun.mockReturnValue({ + data: mockCurrnetMaintenanceRun, + } as any) + when(mockDeckConfigurator) + .calledWith(partialComponentPropsMatcher({ readOnly: true })) + .mockReturnValue(
disabled mock DeckConfigurator
) + const [{ getByText }] = render(props) + getByText('Deck configuration is not available when the robot is busy') + getByText('disabled mock DeckConfigurator') + }) + + it('should render no deck fixtures, if deck configs are not set', () => { + when(mockUseDeckConfigurationQuery) + .calledWith() + .mockReturnValue([] as any) + const [{ getByText }] = render(props) + getByText('No deck fixtures') }) }) diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx index 4bc2c582557..efb04233d0c 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' +import { css } from 'styled-components' import { ALIGN_CENTER, @@ -12,17 +13,21 @@ import { Flex, JUSTIFY_SPACE_BETWEEN, Link, - SIZE_5, SPACING, TYPOGRAPHY, } from '@opentrons/components' import { + useCurrentMaintenanceRun, useDeckConfigurationQuery, useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' import { + getCutoutDisplayName, getFixtureDisplayName, - STANDARD_SLOT_LOAD_NAME, + SINGLE_RIGHT_CUTOUTS, + SINGLE_SLOT_FIXTURES, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import { StyledText } from '../../atoms/text' @@ -31,7 +36,9 @@ import { DeckFixtureSetupInstructionsModal } from './DeckFixtureSetupInstruction import { AddFixtureModal } from './AddFixtureModal' import { useRunStatuses } from '../Devices/hooks' -import type { Cutout } from '@opentrons/shared-data' +import type { CutoutId } from '@opentrons/shared-data' + +const RUN_REFETCH_INTERVAL = 5000 interface DeviceDetailsDeckConfigurationProps { robotName: string @@ -48,37 +55,50 @@ export function DeviceDetailsDeckConfiguration({ const [showAddFixtureModal, setShowAddFixtureModal] = React.useState( false ) - const [ - targetFixtureLocation, - setTargetFixtureLocation, - ] = React.useState(null) + const [targetCutoutId, setTargetCutoutId] = React.useState( + null + ) const deckConfig = useDeckConfigurationQuery().data ?? [] const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() const { isRunRunning } = useRunStatuses() + const { data: maintenanceRunData } = useCurrentMaintenanceRun({ + refetchInterval: RUN_REFETCH_INTERVAL, + }) + const isMaintenanceRunExisting = maintenanceRunData?.data?.id != null - const handleClickAdd = (fixtureLocation: Cutout): void => { - setTargetFixtureLocation(fixtureLocation) + const handleClickAdd = (cutoutId: CutoutId): void => { + setTargetCutoutId(cutoutId) setShowAddFixtureModal(true) } - const handleClickRemove = (fixtureLocation: Cutout): void => { - updateDeckConfiguration({ - fixtureLocation, - loadName: STANDARD_SLOT_LOAD_NAME, - }) + const handleClickRemove = (cutoutId: CutoutId): void => { + const isRightCutout = SINGLE_RIGHT_CUTOUTS.includes(cutoutId) + const singleSlotFixture = isRightCutout + ? SINGLE_RIGHT_SLOT_FIXTURE + : SINGLE_LEFT_SLOT_FIXTURE + + const newDeckConfig = deckConfig.map(fixture => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: singleSlotFixture } + : fixture + ) + + updateDeckConfiguration(newDeckConfig) } // do not show standard slot in fixture display list const fixtureDisplayList = deckConfig.filter( - fixture => fixture.loadName !== STANDARD_SLOT_LOAD_NAME + fixture => + fixture.cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) ) return ( <> - {showAddFixtureModal && targetFixtureLocation != null ? ( + {showAddFixtureModal && targetCutoutId != null ? ( ) : null} @@ -124,16 +144,25 @@ export function DeviceDetailsDeckConfiguration({ gridGap={SPACING.spacing16} paddingX={SPACING.spacing16} paddingBottom={SPACING.spacing32} - paddingTop={isRunRunning ? undefined : SPACING.spacing32} + paddingTop={ + isRunRunning || isMaintenanceRunExisting + ? undefined + : SPACING.spacing32 + } width="100%" flexDirection={DIRECTION_COLUMN} > {isRunRunning ? ( - {t('deck_configuration_is_not_available')} + {t('deck_configuration_is_not_available_when_run_is_in_progress')} + + ) : null} + {isMaintenanceRunExisting ? ( + + {t('deck_configuration_is_not_available_when_robot_is_busy')} ) : null} - + {t('location')} {t('fixture')} - {fixtureDisplayList.map(fixture => { - return ( + {fixtureDisplayList.length > 0 ? ( + fixtureDisplayList.map(fixture => ( - {fixture.fixtureLocation} - {getFixtureDisplayName(fixture.loadName)} + {getCutoutDisplayName(fixture.cutoutId)} + + + {getFixtureDisplayName(fixture.cutoutFixtureId)} - ) - })} + )) + ) : ( + + {t('no_deck_fixtures')} + + )} @@ -185,3 +226,13 @@ export function DeviceDetailsDeckConfiguration({ ) } + +const DECK_CONFIG_SECTION_STYLE = css` + flex-direction: ${DIRECTION_ROW}; + grid-gap: ${SPACING.spacing40}; + @media screen and (max-width: 1024px) { + flex-direction: ${DIRECTION_COLUMN}; + align-items: ${ALIGN_CENTER}; + grid-gap: ${SPACING.spacing32}; + } +` diff --git a/app/src/organisms/Devices/InstrumentsAndModules.tsx b/app/src/organisms/Devices/InstrumentsAndModules.tsx index 523c1cd3510..ef6cb2a7f60 100644 --- a/app/src/organisms/Devices/InstrumentsAndModules.tsx +++ b/app/src/organisms/Devices/InstrumentsAndModules.tsx @@ -105,7 +105,8 @@ export function InstrumentsAndModules({ attachedPipettes?.left ?? null ) const attachPipetteRequired = - attachedLeftPipette == null && attachedRightPipette == null + attachedLeftPipette?.data?.calibratedOffset?.last_modified == null && + attachedRightPipette?.data?.calibratedOffset?.last_modified == null const updatePipetteFWRequired = badLeftPipette != null || badRightPipette != null diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx index 8e2d67fd238..f8dcee3d93b 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx @@ -70,7 +70,6 @@ export const ProtocolRunModuleControls = ({ const { attachPipetteRequired, updatePipetteFWRequired } = usePipetteIsReady() const moduleRenderInfoForProtocolById = useModuleRenderInfoForProtocolById( - robotName, runId ) const attachedModules = Object.values(moduleRenderInfoForProtocolById).filter( diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx index ab5fdaffd3c..244c72e6650 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -1,10 +1,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { - LoadedFixturesBySlot, - parseAllRequiredModuleModels, -} from '@opentrons/api-client' +import { parseAllRequiredModuleModels } from '@opentrons/api-client' import { Flex, ALIGN_CENTER, @@ -17,15 +14,14 @@ import { TYPOGRAPHY, Link, } from '@opentrons/components' -import { - STAGING_AREA_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, -} from '@opentrons/shared-data' import { Line } from '../../../atoms/structure' import { StyledText } from '../../../atoms/text' import { InfoMessage } from '../../../molecules/InfoMessage' +import { + getRequiredDeckConfig, + getSimplestDeckConfigForProtocolCommands, +} from '../../../resources/deck_configuration/utils' import { useIsFlex, useRobot, @@ -73,55 +69,9 @@ export function ProtocolRunSetup({ const { t, i18n } = useTranslation('protocol_setup') const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId) const storedProtocolAnalysis = useStoredProtocolAnalysis(runId) - const protocolData = robotProtocolAnalysis ?? storedProtocolAnalysis - const modules = parseAllRequiredModuleModels(protocolData?.commands ?? []) + const protocolAnalysis = robotProtocolAnalysis ?? storedProtocolAnalysis + const modules = parseAllRequiredModuleModels(protocolAnalysis?.commands ?? []) - // TODO(Jr, 10/4/23): stubbed in the fixtures for now - delete IMMEDIATELY - // const loadedFixturesBySlot = parseInitialLoadedFixturesByCutout( - // protocolData?.commands ?? [] - // ) - - const STUBBED_LOAD_FIXTURE_BY_SLOT: LoadedFixturesBySlot = { - D3: { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'D3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - }, - B3: { - id: 'stubbed_load_fixture_2', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId_2', - loadName: STAGING_AREA_LOAD_NAME, - location: { cutout: 'B3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - }, - C3: { - id: 'stubbed_load_fixture_3', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId_3', - loadName: TRASH_BIN_LOAD_NAME, - location: { cutout: 'C3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - }, - } const robot = useRobot(robotName) const calibrationStatusRobot = useRunCalibrationStatus(robotName, runId) const calibrationStatusModules = useModuleCalibrationStatus(robotName, runId) @@ -133,7 +83,7 @@ export function ProtocolRunSetup({ ) const stepsKeysInOrder = - protocolData != null + protocolAnalysis != null ? [ ROBOT_CALIBRATION_STEP_KEY, MODULE_SETUP_KEY, @@ -144,32 +94,39 @@ export function ProtocolRunSetup({ : [ROBOT_CALIBRATION_STEP_KEY, LPC_KEY, LABWARE_SETUP_KEY] const targetStepKeyInOrder = stepsKeysInOrder.filter((stepKey: StepKey) => { - if (protocolData == null) { + if (protocolAnalysis == null) { return stepKey !== MODULE_SETUP_KEY && stepKey !== LIQUID_SETUP_KEY } if ( - protocolData.modules.length === 0 && - protocolData.liquids.length === 0 + protocolAnalysis.modules.length === 0 && + protocolAnalysis.liquids.length === 0 ) { return stepKey !== MODULE_SETUP_KEY && stepKey !== LIQUID_SETUP_KEY } - if (protocolData.modules.length === 0) { + if (protocolAnalysis.modules.length === 0) { return stepKey !== MODULE_SETUP_KEY } - if (protocolData.liquids.length === 0) { + if (protocolAnalysis.liquids.length === 0) { return stepKey !== LIQUID_SETUP_KEY } return true }) if (robot == null) return null - const hasLiquids = protocolData != null && protocolData.liquids?.length > 0 - const hasModules = protocolData != null && modules.length > 0 - const hasFixtures = - protocolData != null && Object.keys(STUBBED_LOAD_FIXTURE_BY_SLOT).length > 0 + const hasLiquids = + protocolAnalysis != null && protocolAnalysis.liquids?.length > 0 + const hasModules = protocolAnalysis != null && modules.length > 0 + + const protocolDeckConfig = getSimplestDeckConfigForProtocolCommands( + protocolAnalysis?.commands ?? [] + ) + + const requiredDeckConfig = getRequiredDeckConfig(protocolDeckConfig) + + const hasFixtures = requiredDeckConfig.length > 0 let moduleDescription: string = t(`${MODULE_SETUP_KEY}_description`, { count: modules.length, @@ -213,8 +170,8 @@ export function ProtocolRunSetup({ expandLabwarePositionCheckStep={() => setExpandedStepKey(LPC_KEY)} robotName={robotName} runId={runId} - loadedFixturesBySlot={STUBBED_LOAD_FIXTURE_BY_SLOT} hasModules={hasModules} + commands={protocolAnalysis?.commands ?? []} /> ), description: moduleDescription, @@ -251,6 +208,7 @@ export function ProtocolRunSetup({ protocolRunHeaderRef={protocolRunHeaderRef} robotName={robotName} runId={runId} + protocolAnalysis={protocolAnalysis} /> ), description: hasLiquids @@ -265,7 +223,7 @@ export function ProtocolRunSetup({ gridGap={SPACING.spacing16} margin={SPACING.spacing16} > - {protocolData != null ? ( + {protocolAnalysis != null ? ( <> {runHasStarted ? ( diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx index d91a0673d32..b82c3d7b302 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/LabwareListItem.tsx @@ -93,10 +93,18 @@ export function LabwareListItem( const { createLiveCommand } = useCreateLiveCommandMutation() const [isLatchLoading, setIsLatchLoading] = React.useState(false) const [isLatchClosed, setIsLatchClosed] = React.useState(false) - let slotInfo: string | null = - initialLocation !== 'offDeck' && 'slotName' in initialLocation - ? initialLocation.slotName - : null + + let slotInfo: string | null = null + + if (initialLocation !== 'offDeck' && 'slotName' in initialLocation) { + slotInfo = initialLocation.slotName + } else if ( + initialLocation !== 'offDeck' && + 'addressableAreaName' in initialLocation + ) { + slotInfo = initialLocation.addressableAreaName + } + let moduleDisplayName: string | null = null let extraAttentionText: JSX.Element | null = null let isCorrectHeaterShakerAttached: boolean = false diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx index 0225ff8e575..528b293a390 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx @@ -12,12 +12,11 @@ import { import { FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, THERMOCYCLER_MODULE_V1, } from '@opentrons/shared-data' import { getLabwareSetupItemGroups } from '../../../../pages/Protocols/utils' -import { getDeckConfigFromProtocolCommands } from '../../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../../resources/deck_configuration/utils' import { getAttachedProtocolModuleMatches } from '../../../ProtocolSetupModulesAndDeck/utils' import { useAttachedModules } from '../../hooks' import { LabwareInfoOverlay } from '../LabwareInfoOverlay' @@ -52,7 +51,7 @@ export function SetupLabwareMap({ const commands = protocolAnalysis.commands - const robotType = getRobotTypeFromLoadedLabware(protocolAnalysis.labware) + const robotType = protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE const deckDef = getDeckDefFromRobotType(robotType) const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef) @@ -107,7 +106,7 @@ export function SetupLabwareMap({ const { offDeckItems } = getLabwareSetupItemGroups(commands) - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( protocolAnalysis.commands ) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx index 079ef1fcdb3..e6fabcac8ad 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabwareMap.test.tsx @@ -3,12 +3,11 @@ import { when, resetAllWhenMocks } from 'jest-when' import { StaticRouter } from 'react-router-dom' import { renderWithProviders, - componentPropsMatcher, partialComponentPropsMatcher, LabwareRender, Module, } from '@opentrons/components' -import { getModuleDef2 } from '@opentrons/shared-data' +import { OT2_ROBOT_TYPE, getModuleDef2 } from '@opentrons/shared-data' import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' import { i18n } from '../../../../../i18n' @@ -111,7 +110,7 @@ describe('SetupLabwareMap', () => { when(mockLabwareRender) .mockReturnValue(
) // this (default) empty div will be returned when LabwareRender isn't called with expected labware definition .calledWith( - componentPropsMatcher({ + partialComponentPropsMatcher({ definition: fixture_tiprack_300_ul, }) ) @@ -160,6 +159,7 @@ describe('SetupLabwareMap', () => { protocolAnalysis: ({ commands: [], labware: [], + robotType: OT2_ROBOT_TYPE, } as unknown) as CompletedProtocolAnalysis, }) @@ -237,6 +237,7 @@ describe('SetupLabwareMap', () => { protocolAnalysis: ({ commands: [], labware: [], + robotType: OT2_ROBOT_TYPE, } as unknown) as CompletedProtocolAnalysis, }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/index.tsx index 6ee3657de84..82dba6c6cc6 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/index.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/index.tsx @@ -42,10 +42,7 @@ export function SetupLabware(props: SetupLabwareProps): JSX.Element { ) const isFlex = useIsFlex(robotName) - const moduleRenderInfoById = useModuleRenderInfoForProtocolById( - robotName, - runId - ) + const moduleRenderInfoById = useModuleRenderInfoForProtocolById(runId) const moduleModels = map( moduleRenderInfoById, ({ moduleDef }) => moduleDef.model diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx index 5bc7e4eb54f..679a7afed6c 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx @@ -2,244 +2,207 @@ import * as React from 'react' import map from 'lodash/map' import isEmpty from 'lodash/isEmpty' import { - parseLiquidsInLoadOrder, - parseLabwareInfoByLiquidId, parseInitialLoadedLabwareByAdapter, + parseLabwareInfoByLiquidId, + parseLiquidsInLoadOrder, } from '@opentrons/api-client' import { + ALIGN_CENTER, + BaseDeck, DIRECTION_COLUMN, Flex, - RobotWorkSpace, - SlotLabels, - LabwareRender, - Module, - ALIGN_CENTER, JUSTIFY_CENTER, + LabwareRender, } from '@opentrons/components' import { + FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - inferModuleOrientationFromXCoordinate, THERMOCYCLER_MODULE_V1, } from '@opentrons/shared-data' -import { - useLabwareRenderInfoForRunById, - useModuleRenderInfoForProtocolById, - useProtocolDetailsForRun, -} from '../../hooks' -import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { useAttachedModules } from '../../hooks' import { LabwareInfoOverlay } from '../LabwareInfoOverlay' -import { getStandardDeckViewLayerBlockList } from '../utils/getStandardDeckViewLayerBlockList' import { LiquidsLabwareDetailsModal } from './LiquidsLabwareDetailsModal' import { getWellFillFromLabwareId } from './utils' -import type { RobotType } from '@opentrons/shared-data' +import { getLabwareRenderInfo } from '../utils/getLabwareRenderInfo' +import { getSimplestDeckConfigForProtocolCommands } from '../../../../resources/deck_configuration/utils' +import { getStandardDeckViewLayerBlockList } from '../utils/getStandardDeckViewLayerBlockList' +import { getAttachedProtocolModuleMatches } from '../../../ProtocolSetupModulesAndDeck/utils' +import { getProtocolModulesInfo } from '../utils/getProtocolModulesInfo' + +import type { + CompletedProtocolAnalysis, + ProtocolAnalysisOutput, +} from '@opentrons/shared-data' -const OT2_VIEWBOX = '-80 -40 550 500' -const OT3_VIEWBOX = '-144.31 -76.59 750 681.74' +const ATTACHED_MODULE_POLL_MS = 5000 -const getViewBox = (robotType: RobotType): string | null => { - switch (robotType) { - case 'OT-2 Standard': - return OT2_VIEWBOX - case 'OT-3 Standard': - return OT3_VIEWBOX - default: - return null - } -} interface SetupLiquidsMapProps { runId: string - robotName: string + protocolAnalysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput | null } -export function SetupLiquidsMap(props: SetupLiquidsMapProps): JSX.Element { - const { runId, robotName } = props +export function SetupLiquidsMap( + props: SetupLiquidsMapProps +): JSX.Element | null { + const { runId, protocolAnalysis } = props const [hoverLabwareId, setHoverLabwareId] = React.useState('') + const [liquidDetailsLabwareId, setLiquidDetailsLabwareId] = React.useState< + string | null + >(null) + const attachedModules = + useAttachedModules({ + refetchInterval: ATTACHED_MODULE_POLL_MS, + }) ?? [] + + if (protocolAnalysis == null) return null - const moduleRenderInfoById = useModuleRenderInfoForProtocolById( - robotName, - runId - ) - const labwareRenderInfoById = useLabwareRenderInfoForRunById(runId) - const { robotType } = useProtocolDetailsForRun(runId) - const protocolData = useMostRecentCompletedAnalysis(runId) const liquids = parseLiquidsInLoadOrder( - protocolData?.liquids != null ? protocolData?.liquids : [], - protocolData?.commands ?? [] + protocolAnalysis.liquids != null ? protocolAnalysis.liquids : [], + protocolAnalysis.commands ?? [] ) const initialLoadedLabwareByAdapter = parseInitialLoadedLabwareByAdapter( - protocolData?.commands ?? [] + protocolAnalysis.commands ?? [] ) + const robotType = protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE const deckDef = getDeckDefFromRobotType(robotType) + const labwareRenderInfo = getLabwareRenderInfo(protocolAnalysis, deckDef) const labwareByLiquidId = parseLabwareInfoByLiquidId( - protocolData?.commands ?? [] + protocolAnalysis.commands ?? [] ) - const [liquidDetailsLabwareId, setLiquidDetailsLabwareId] = React.useState< - string | null - >(null) + const deckConfig = getSimplestDeckConfigForProtocolCommands( + protocolAnalysis.commands + ) + const deckLayerBlocklist = getStandardDeckViewLayerBlockList(robotType) + + const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef) + + const attachedProtocolModuleMatches = getAttachedProtocolModuleMatches( + attachedModules, + protocolModulesInfo + ) + + const moduleLocations = attachedProtocolModuleMatches.map(module => { + const labwareInAdapterInMod = + module.nestedLabwareId != null + ? initialLoadedLabwareByAdapter[module.nestedLabwareId] + : null + // only rendering the labware on top most layer so + // either the adapter or the labware are rendered but not both + const topLabwareDefinition = + labwareInAdapterInMod?.result?.definition ?? module.nestedLabwareDef + const topLabwareId = + labwareInAdapterInMod?.result?.labwareId ?? module.nestedLabwareId + const topLabwareDisplayName = + labwareInAdapterInMod?.params.displayName ?? + module.nestedLabwareDisplayName + const nestedLabwareWellFill = getWellFillFromLabwareId( + module.nestedLabwareId ?? '', + liquids, + labwareByLiquidId + ) + const labwareHasLiquid = !isEmpty(nestedLabwareWellFill) + + return { + moduleModel: module.moduleDef.model, + moduleLocation: { slotName: module.slotName }, + innerProps: + module.moduleDef.model === THERMOCYCLER_MODULE_V1 + ? { lidMotorState: 'open' } + : {}, + + nestedLabwareDef: topLabwareDefinition, + nestedLabwareWellFill, + moduleChildren: + topLabwareDefinition != null && topLabwareId != null ? ( + setHoverLabwareId(topLabwareId)} + onMouseLeave={() => setHoverLabwareId('')} + onClick={() => + labwareHasLiquid ? setLiquidDetailsLabwareId(topLabwareId) : null + } + cursor={labwareHasLiquid ? 'pointer' : ''} + > + + + ) : null, + } + }) return ( - - {() => ( - <> - {map( - moduleRenderInfoById, - ({ - x, - y, - moduleDef, - nestedLabwareDef, - nestedLabwareId, - nestedLabwareDisplayName, - moduleId, - }) => { - const labwareInAdapterInMod = - nestedLabwareId != null - ? initialLoadedLabwareByAdapter[nestedLabwareId] - : null - // only rendering the labware on top most layer so - // either the adapter or the labware are rendered but not both - const topLabwareDefinition = - labwareInAdapterInMod?.result?.definition ?? nestedLabwareDef - const topLabwareId = - labwareInAdapterInMod?.result?.labwareId ?? nestedLabwareId - const topLabwareDisplayName = - labwareInAdapterInMod?.params.displayName ?? - nestedLabwareDisplayName - - const wellFill = getWellFillFromLabwareId( - topLabwareId ?? '', - liquids, - labwareByLiquidId - ) - const labwareHasLiquid = !isEmpty(wellFill) - - return ( - - {topLabwareDefinition != null && - topLabwareDisplayName != null && - topLabwareId != null ? ( - - setHoverLabwareId(topLabwareId)} - onMouseLeave={() => setHoverLabwareId('')} - onClick={() => - labwareHasLiquid - ? setLiquidDetailsLabwareId(topLabwareId) - : null - } - cursor={labwareHasLiquid ? 'pointer' : ''} - > - - - - - ) : null} - - ) - } - )} - {map( - labwareRenderInfoById, - ({ x, y, labwareDef, displayName }, labwareId) => { - const labwareInAdapter = - initialLoadedLabwareByAdapter[labwareId] - // only rendering the labware on top most layer so - // either the adapter or the labware are rendered but not both - const topLabwareDefinition = - labwareInAdapter?.result?.definition ?? labwareDef - const topLabwareId = - labwareInAdapter?.result?.labwareId ?? labwareId - const topLabwareDisplayName = - labwareInAdapter?.params.displayName ?? displayName - const wellFill = getWellFillFromLabwareId( - topLabwareId ?? '', - liquids, - labwareByLiquidId - ) - const labwareHasLiquid = !isEmpty(wellFill) - return ( - - setHoverLabwareId(topLabwareId)} - onMouseLeave={() => setHoverLabwareId('')} - onClick={() => - labwareHasLiquid - ? setLiquidDetailsLabwareId(topLabwareId) - : null - } - cursor={labwareHasLiquid ? 'pointer' : ''} - > - - - - - ) - } - )} - - + {map( + labwareRenderInfo, + ({ x, y, labwareDef, displayName }, labwareId) => { + const labwareInAdapter = initialLoadedLabwareByAdapter[labwareId] + // only rendering the labware on top most layer so + // either the adapter or the labware are rendered but not both + const topLabwareDefinition = + labwareInAdapter?.result?.definition ?? labwareDef + const topLabwareId = + labwareInAdapter?.result?.labwareId ?? labwareId + const topLabwareDisplayName = + labwareInAdapter?.params.displayName ?? displayName + const wellFill = getWellFillFromLabwareId( + topLabwareId ?? '', + liquids, + labwareByLiquidId + ) + const labwareHasLiquid = !isEmpty(wellFill) + return ( + + setHoverLabwareId(topLabwareId)} + onMouseLeave={() => setHoverLabwareId('')} + onClick={() => + labwareHasLiquid + ? setLiquidDetailsLabwareId(topLabwareId) + : null + } + cursor={labwareHasLiquid ? 'pointer' : ''} + > + + + + + ) + } )} - + {liquidDetailsLabwareId != null && ( ) => { return renderWithProviders( - , + , { i18nInstance: i18n, } diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx index 51ded61559c..8d17c44f7b1 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx @@ -2,88 +2,104 @@ import * as React from 'react' import { when, resetAllWhenMocks } from 'jest-when' import { i18n } from '../../../../../i18n' import { + BaseDeck, renderWithProviders, partialComponentPropsMatcher, - RobotWorkSpace, LabwareRender, - Module, } from '@opentrons/components' + +import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' import { - inferModuleOrientationFromXCoordinate, - LabwareDefinition2, - ModuleModel, - ModuleType, + FLEX_ROBOT_TYPE, + getDeckDefFromRobotType, + OT2_ROBOT_TYPE, } from '@opentrons/shared-data' -import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' -import standardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot2_standard.json' import { - useLabwareRenderInfoForRunById, - useModuleRenderInfoForProtocolById, - useProtocolDetailsForRun, -} from '../../../hooks' -import { getWellFillFromLabwareId } from '../utils' -import { SetupLiquidsMap } from '../SetupLiquidsMap' + parseInitialLoadedLabwareByAdapter, + parseLabwareInfoByLiquidId, + parseLiquidsInLoadOrder, + simpleAnalysisFileFixture, +} from '@opentrons/api-client' +import ot2StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot2_standard.json' +import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json' + +import { useAttachedModules } from '../../../hooks' import { LabwareInfoOverlay } from '../../LabwareInfoOverlay' +import { getLabwareRenderInfo } from '../../utils/getLabwareRenderInfo' +import { getStandardDeckViewLayerBlockList } from '../../utils/getStandardDeckViewLayerBlockList' +import { getAttachedProtocolModuleMatches } from '../../../../ProtocolSetupModulesAndDeck/utils' +import { getProtocolModulesInfo } from '../../utils/getProtocolModulesInfo' +import { getSimplestDeckConfigForProtocolCommands } from '../../../../../resources/deck_configuration/utils' +import { mockProtocolModuleInfo } from '../../../../ProtocolSetupLabware/__fixtures__' +import { mockFetchModulesSuccessActionPayloadModules } from '../../../../../redux/modules/__fixtures__' + +import { SetupLiquidsMap } from '../SetupLiquidsMap' + +import type { + ModuleModel, + ModuleType, + RunTimeCommand, + LabwareDefinition2, +} from '@opentrons/shared-data' jest.mock('@opentrons/components', () => { const actualComponents = jest.requireActual('@opentrons/components') return { ...actualComponents, - Module: jest.fn(() =>
mock Module
), - RobotWorkSpace: jest.fn(() =>
mock RobotWorkSpace
), LabwareRender: jest.fn(() =>
mock LabwareRender
), } }) -jest.mock('@opentrons/shared-data', () => { - const actualSharedData = jest.requireActual('@opentrons/shared-data') - return { - ...actualSharedData, - inferModuleOrientationFromXCoordinate: jest.fn(), - } -}) + +jest.mock('@opentrons/components/src/hardware-sim/BaseDeck') +jest.mock('@opentrons/api-client') +jest.mock('@opentrons/shared-data/js/helpers') jest.mock('../../LabwareInfoOverlay') jest.mock('../../../hooks') jest.mock('../utils') +jest.mock('../../utils/getLabwareRenderInfo') +jest.mock('../../../../ProtocolSetupModulesAndDeck/utils') +jest.mock('../../utils/getProtocolModulesInfo') +jest.mock('../../../../../resources/deck_configuration/utils') -const mockUseProtocolDetailsForRun = useProtocolDetailsForRun as jest.MockedFunction< - typeof useProtocolDetailsForRun +const mockUseAttachedModules = useAttachedModules as jest.MockedFunction< + typeof useAttachedModules > const mockLabwareInfoOverlay = LabwareInfoOverlay as jest.MockedFunction< typeof LabwareInfoOverlay > -const mockModule = Module as jest.MockedFunction -const mockInferModuleOrientationFromXCoordinate = inferModuleOrientationFromXCoordinate as jest.MockedFunction< - typeof inferModuleOrientationFromXCoordinate -> -const mockRobotWorkSpace = RobotWorkSpace as jest.MockedFunction< - typeof RobotWorkSpace -> const mockLabwareRender = LabwareRender as jest.MockedFunction< typeof LabwareRender > -const mockUseLabwareRenderInfoForRunById = useLabwareRenderInfoForRunById as jest.MockedFunction< - typeof useLabwareRenderInfoForRunById +const mockBaseDeck = BaseDeck as jest.MockedFunction +const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction< + typeof getDeckDefFromRobotType > -const mockUseModuleRenderInfoForProtocolById = useModuleRenderInfoForProtocolById as jest.MockedFunction< - typeof useModuleRenderInfoForProtocolById +const mockParseInitialLoadedLabwareByAdapter = parseInitialLoadedLabwareByAdapter as jest.MockedFunction< + typeof parseInitialLoadedLabwareByAdapter > -const mockGetWellFillFromLabwareId = getWellFillFromLabwareId as jest.MockedFunction< - typeof getWellFillFromLabwareId +const mockParseLabwareInfoByLiquidId = parseLabwareInfoByLiquidId as jest.MockedFunction< + typeof parseLabwareInfoByLiquidId +> +const mockParseLiquidsInLoadOrder = parseLiquidsInLoadOrder as jest.MockedFunction< + typeof parseLiquidsInLoadOrder +> +const mockGetLabwareRenderInfo = getLabwareRenderInfo as jest.MockedFunction< + typeof getLabwareRenderInfo +> +const mockGetAttachedProtocolModuleMatches = getAttachedProtocolModuleMatches as jest.MockedFunction< + typeof getAttachedProtocolModuleMatches +> +const mockGetProtocolModulesInfo = getProtocolModulesInfo as jest.MockedFunction< + typeof getProtocolModulesInfo +> +const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction< + typeof getSimplestDeckConfigForProtocolCommands > -const MOCK_WELL_FILL = { C1: '#ff4888', C2: '#ff4888' } - -const deckSlotsById = standardDeckDef.locations.orderedSlots.reduce( - (acc, deckSlot) => ({ ...acc, [deckSlot.id]: deckSlot }), - {} -) - -const ROBOT_NAME = 'otie' const RUN_ID = '1' -const STUBBED_ORIENTATION_VALUE = 'left' const MOCK_300_UL_TIPRACK_ID = '300_ul_tiprack_id' const MOCK_MAGNETIC_MODULE_COORDS = [10, 20, 0] -const MOCK_TC_COORDS = [20, 30, 0] +const MOCK_SECOND_MAGNETIC_MODULE_COORDS = [100, 200, 0] const MOCK_300_UL_TIPRACK_COORDS = [30, 40, 0] const mockMagneticModule = { @@ -106,26 +122,23 @@ const mockMagneticModule = { quirks: [], } -const mockTCModule = { - labwareOffset: { x: 3, y: 3, z: 3 }, - moduleId: 'TCModuleId', - model: 'thermocyclerModuleV1' as ModuleModel, - type: 'thermocyclerModuleType' as ModuleType, -} - const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, }) } +const mockProtocolAnalysis = { + ...simpleAnalysisFileFixture, + robotType: OT2_ROBOT_TYPE, +} as any describe('SetupLiquidsMap', () => { let props: React.ComponentProps beforeEach(() => { - props = { runId: RUN_ID, robotName: ROBOT_NAME } - when(mockInferModuleOrientationFromXCoordinate) - .calledWith(expect.anything()) - .mockReturnValue(STUBBED_ORIENTATION_VALUE) + props = { + runId: RUN_ID, + protocolAnalysis: mockProtocolAnalysis, + } when(mockLabwareRender) .mockReturnValue(
) // this (default) empty div will be returned when LabwareRender isn't called with expected labware definition .calledWith( @@ -145,14 +158,28 @@ describe('SetupLiquidsMap', () => { }) ) .mockReturnValue(
mock labware render with well fill
) - + when(mockUseAttachedModules).calledWith().mockReturnValue([]) + when(mockGetAttachedProtocolModuleMatches).mockReturnValue([]) + when(mockGetLabwareRenderInfo) + .calledWith(mockProtocolAnalysis, ot2StandardDeckDef as any) + .mockReturnValue({}) + when(mockGetSimplestDeckConfigForProtocolCommands) + .calledWith(mockProtocolAnalysis.commands as RunTimeCommand[]) + // TODO(bh, 2023-11-13): mock the cutout config protocol spec + .mockReturnValue([]) + when(mockParseLiquidsInLoadOrder) + .calledWith( + mockProtocolAnalysis.liquids as any, + mockProtocolAnalysis.commands as any + ) + .mockReturnValue([]) + when(mockParseInitialLoadedLabwareByAdapter) + .calledWith(mockProtocolAnalysis.commands as any) + .mockReturnValue({}) when(mockLabwareInfoOverlay) .mockReturnValue(
) // this (default) empty div will be returned when LabwareInfoOverlay isn't called with expected props .calledWith( - partialComponentPropsMatcher({ - definition: fixture_tiprack_300_ul, - labwareHasLiquid: false, - }) + partialComponentPropsMatcher({ definition: fixture_tiprack_300_ul }) ) .mockReturnValue(
@@ -160,94 +187,101 @@ describe('SetupLiquidsMap', () => { {fixture_tiprack_300_ul.metadata.displayName}
) - .calledWith(partialComponentPropsMatcher({ labwareHasLiquid: true })) - .mockReturnValue(
mock labware overlay with liquid
) + }) - when(mockRobotWorkSpace) - .mockReturnValue(
) // this (default) empty div will be returned when RobotWorkSpace isn't called with expected props - .calledWith( - partialComponentPropsMatcher({ - deckDef: standardDeckDef, - children: expect.anything(), - }) - ) - .mockImplementation(({ children }) => ( - - {/* @ts-expect-error children won't be null since we checked for expect.anything() above */} - {children({ - deckSlotsById, - getRobotCoordsFromDOMCoords: {} as any, - })} - - )) - when(mockUseProtocolDetailsForRun) - .calledWith(RUN_ID) - .mockReturnValue({ - protocolData: { - pipettes: {}, - labware: {}, - modules: { - heatershaker_id: { - model: 'heaterShakerModuleV1', - }, - }, - liquids: [ - { - id: '1', - displayName: 'mock liquid', - description: '', - displayColor: '#FFFFFF', - }, - ], - labwareDefinitions: {}, - commands: [], - }, - } as any) + afterEach(() => { + jest.clearAllMocks() + resetAllWhenMocks() }) - afterEach(() => resetAllWhenMocks()) it('should render a deck WITHOUT labware and WITHOUT modules', () => { - when(mockUseLabwareRenderInfoForRunById) - .calledWith(RUN_ID) - .mockReturnValue({}) - when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) - .mockReturnValue({}) - + props = { + ...props, + protocolAnalysis: null, + } render(props) - expect(mockModule).not.toHaveBeenCalled() expect(mockLabwareRender).not.toHaveBeenCalled() expect(mockLabwareInfoOverlay).not.toHaveBeenCalled() }) - it('should render a deck WITH labware and WITHOUT modules', () => { - when(mockUseLabwareRenderInfoForRunById) - .calledWith(RUN_ID) - .mockReturnValue({ - '300_ul_tiprack_id': { - labwareDef: fixture_tiprack_300_ul as LabwareDefinition2, - displayName: 'fresh tips', - x: MOCK_300_UL_TIPRACK_COORDS[0], - y: MOCK_300_UL_TIPRACK_COORDS[1], - z: MOCK_300_UL_TIPRACK_COORDS[2], - slotName: '1', - }, - }) - when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) + it('should render base deck - robot type is OT-2', () => { + when(mockGetDeckDefFromRobotType) + .calledWith(OT2_ROBOT_TYPE) + .mockReturnValue(ot2StandardDeckDef as any) + when(mockParseLabwareInfoByLiquidId) + .calledWith(mockProtocolAnalysis.commands as any) .mockReturnValue({}) + mockUseAttachedModules.mockReturnValue( + mockFetchModulesSuccessActionPayloadModules + ) + when(mockGetLabwareRenderInfo).mockReturnValue({}) + when(mockGetProtocolModulesInfo) + .calledWith(mockProtocolAnalysis, ot2StandardDeckDef as any) + .mockReturnValue(mockProtocolModuleInfo) + when(mockGetAttachedProtocolModuleMatches) + .calledWith( + mockFetchModulesSuccessActionPayloadModules, + mockProtocolModuleInfo + ) + .mockReturnValue([ + { + moduleId: mockMagneticModule.moduleId, + x: MOCK_MAGNETIC_MODULE_COORDS[0], + y: MOCK_MAGNETIC_MODULE_COORDS[1], + z: MOCK_MAGNETIC_MODULE_COORDS[2], + moduleDef: mockMagneticModule as any, + nestedLabwareDef: null, + nestedLabwareDisplayName: null, + nestedLabwareId: null, + slotName: '1', + protocolLoadOrder: 1, + attachedModuleMatch: null, + }, + { + moduleId: mockMagneticModule.moduleId, + x: MOCK_SECOND_MAGNETIC_MODULE_COORDS[0], + y: MOCK_SECOND_MAGNETIC_MODULE_COORDS[1], + z: MOCK_SECOND_MAGNETIC_MODULE_COORDS[2], + moduleDef: mockMagneticModule as any, + nestedLabwareDef: null, + nestedLabwareDisplayName: null, + nestedLabwareId: null, + slotName: '2', + protocolLoadOrder: 0, + attachedModuleMatch: null, + }, + ]) + when(mockBaseDeck) + .calledWith( + partialComponentPropsMatcher({ + robotType: OT2_ROBOT_TYPE, + deckLayerBlocklist: getStandardDeckViewLayerBlockList(OT2_ROBOT_TYPE), + }) + ) + .mockReturnValue(
mock BaseDeck
) const [{ getByText }] = render(props) - expect(mockModule).not.toHaveBeenCalled() - expect(mockLabwareRender).toHaveBeenCalled() - expect(mockLabwareInfoOverlay).toHaveBeenCalled() - getByText('mock labware render of 300ul Tiprack FIXTURE') - getByText('mock labware info overlay of 300ul Tiprack FIXTURE') + getByText('mock BaseDeck') }) - it('should render a deck WITH labware and WITH modules', () => { - when(mockUseLabwareRenderInfoForRunById) - .calledWith(RUN_ID) + it('should render base deck - robot type is Flex', () => { + const mockFlexAnalysis = { + ...mockProtocolAnalysis, + robotType: FLEX_ROBOT_TYPE, + } + props = { + ...props, + protocolAnalysis: { + ...mockProtocolAnalysis, + robotType: FLEX_ROBOT_TYPE, + }, + } + when(mockGetDeckDefFromRobotType) + .calledWith(FLEX_ROBOT_TYPE) + .mockReturnValue(ot3StandardDeckDef as any) + + when(mockGetLabwareRenderInfo) + .calledWith(mockFlexAnalysis, ot3StandardDeckDef as any) .mockReturnValue({ [MOCK_300_UL_TIPRACK_ID]: { labwareDef: fixture_tiprack_300_ul as LabwareDefinition2, @@ -255,79 +289,74 @@ describe('SetupLiquidsMap', () => { x: MOCK_300_UL_TIPRACK_COORDS[0], y: MOCK_300_UL_TIPRACK_COORDS[1], z: MOCK_300_UL_TIPRACK_COORDS[2], - slotName: '1', + slotName: 'C1', }, }) - when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) - .mockReturnValue({ - [mockMagneticModule.moduleId]: { + when(mockParseLabwareInfoByLiquidId) + .calledWith(mockFlexAnalysis.commands as any) + .mockReturnValue({}) + mockUseAttachedModules.mockReturnValue( + mockFetchModulesSuccessActionPayloadModules + ) + + when(mockGetProtocolModulesInfo) + .calledWith(mockFlexAnalysis, ot3StandardDeckDef as any) + .mockReturnValue(mockProtocolModuleInfo) + when(mockGetAttachedProtocolModuleMatches) + .calledWith( + mockFetchModulesSuccessActionPayloadModules, + mockProtocolModuleInfo + ) + .mockReturnValue([ + { moduleId: mockMagneticModule.moduleId, x: MOCK_MAGNETIC_MODULE_COORDS[0], y: MOCK_MAGNETIC_MODULE_COORDS[1], z: MOCK_MAGNETIC_MODULE_COORDS[2], moduleDef: mockMagneticModule as any, nestedLabwareDef: null, + nestedLabwareDisplayName: null, nestedLabwareId: null, - protocolLoadOrder: 0, + slotName: 'C1', + protocolLoadOrder: 1, attachedModuleMatch: null, }, - [mockTCModule.moduleId]: { - moduleId: mockTCModule.moduleId, - x: MOCK_TC_COORDS[0], - y: MOCK_TC_COORDS[1], - z: MOCK_TC_COORDS[2], - moduleDef: mockTCModule, + { + moduleId: mockMagneticModule.moduleId, + x: MOCK_SECOND_MAGNETIC_MODULE_COORDS[0], + y: MOCK_SECOND_MAGNETIC_MODULE_COORDS[1], + z: MOCK_SECOND_MAGNETIC_MODULE_COORDS[2], + moduleDef: mockMagneticModule as any, nestedLabwareDef: null, + nestedLabwareDisplayName: null, nestedLabwareId: null, - protocolLoadOrder: 1, + slotName: 'B1', + protocolLoadOrder: 0, attachedModuleMatch: null, }, - } as any) - - when(mockModule) - .calledWith( - partialComponentPropsMatcher({ - def: mockMagneticModule, - x: MOCK_MAGNETIC_MODULE_COORDS[0], - y: MOCK_MAGNETIC_MODULE_COORDS[1], - }) - ) - .mockReturnValue(
mock module viz {mockMagneticModule.type}
) + ]) - when(mockModule) + when(mockBaseDeck) .calledWith( partialComponentPropsMatcher({ - def: mockTCModule, - x: MOCK_TC_COORDS[0], - y: MOCK_TC_COORDS[1], + deckLayerBlocklist: getStandardDeckViewLayerBlockList( + FLEX_ROBOT_TYPE + ), + robotType: FLEX_ROBOT_TYPE, + // ToDo (kk:11/03/2023) Update the following part later + labwareLocations: expect.anything(), + moduleLocations: expect.anything(), }) ) - .mockReturnValue(
mock module viz {mockTCModule.type}
) - + .mockReturnValue(
mock BaseDeck
) const [{ getByText }] = render(props) - getByText('mock module viz magneticModuleType') - getByText('mock module viz thermocyclerModuleType') - getByText('mock labware render of 300ul Tiprack FIXTURE') - getByText('mock labware info overlay of 300ul Tiprack FIXTURE') - }) - it('should render labware overlay and labware render with liquids', () => { - when(mockUseLabwareRenderInfoForRunById) - .calledWith(RUN_ID) - .mockReturnValue({ - '300_ul_tiprack_id': { - labwareDef: fixture_tiprack_300_ul as LabwareDefinition2, - displayName: 'fresh tips', - x: MOCK_300_UL_TIPRACK_COORDS[0], - y: MOCK_300_UL_TIPRACK_COORDS[1], - z: MOCK_300_UL_TIPRACK_COORDS[2], - slotName: '1', - }, - }) - mockGetWellFillFromLabwareId.mockReturnValue(MOCK_WELL_FILL) - const [{ getByText }] = render({ ...props }) - getByText('mock labware overlay with liquid') - getByText('mock labware render with well fill') + getByText('mock BaseDeck') }) + + // ToDo (kk:11/03/2023) + // The current component implementation is tough to test everything. + // I will do refactoring later and add tests to cover more cases. + // Probably I will replace BaseDeck's children with a new component and write test for that. + it.todo('should render labware overlay and labware render with liquids') }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/index.tsx index 85e2e2761fb..bb4de2d50ea 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/index.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/index.tsx @@ -13,13 +13,24 @@ import { BackToTopButton } from '../BackToTopButton' import { SetupLiquidsList } from './SetupLiquidsList' import { SetupLiquidsMap } from './SetupLiquidsMap' +import type { + CompletedProtocolAnalysis, + ProtocolAnalysisOutput, +} from '@opentrons/shared-data' + interface SetupLiquidsProps { protocolRunHeaderRef: React.RefObject | null robotName: string runId: string + protocolAnalysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput | null } -export function SetupLiquids(props: SetupLiquidsProps): JSX.Element { +export function SetupLiquids({ + protocolRunHeaderRef, + robotName, + runId, + protocolAnalysis, +}: SetupLiquidsProps): JSX.Element { const { t } = useTranslation('protocol_setup') const [selectedValue, toggleGroup] = useToggleGroup( t('list_view'), @@ -35,15 +46,15 @@ export function SetupLiquids(props: SetupLiquidsProps): JSX.Element { > {toggleGroup} {selectedValue === t('list_view') ? ( - + ) : ( - + )} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx index c2ac2721c19..10f3dfeb7e3 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx @@ -21,9 +21,12 @@ import { BORDERS, } from '@opentrons/components' import { + getCutoutDisplayName, getFixtureDisplayName, getModuleDisplayName, - STANDARD_SLOT_LOAD_NAME, + SINGLE_RIGHT_CUTOUTS, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import { Portal } from '../../../../App/portal' import { LegacyModal } from '../../../../molecules/LegacyModal' @@ -32,16 +35,16 @@ import { Modal } from '../../../../molecules/Modal' import { SmallButton } from '../../../../atoms/buttons/SmallButton' import type { - Cutout, - Fixture, - FixtureLoadName, + CutoutConfig, + CutoutId, + CutoutFixtureId, ModuleModel, } from '@opentrons/shared-data' interface LocationConflictModalProps { onCloseClick: () => void - cutout: Cutout - requiredFixture?: FixtureLoadName + cutoutId: CutoutId + requiredFixtureId?: CutoutFixtureId requiredModule?: ModuleModel isOnDevice?: boolean } @@ -51,33 +54,44 @@ export const LocationConflictModal = ( ): JSX.Element => { const { onCloseClick, - cutout, - requiredFixture, + cutoutId, + requiredFixtureId, requiredModule, isOnDevice = false, } = props const { t, i18n } = useTranslation(['protocol_setup', 'shared']) const deckConfig = useDeckConfigurationQuery().data ?? [] const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() - const deckConfigurationAtLocationLoadName = deckConfig.find( - (deckFixture: Fixture) => deckFixture.fixtureLocation === cutout - )?.loadName + const deckConfigurationAtLocationFixtureId = deckConfig.find( + (deckFixture: CutoutConfig) => deckFixture.cutoutId === cutoutId + )?.cutoutFixtureId const currentFixtureDisplayName = - deckConfigurationAtLocationLoadName != null - ? getFixtureDisplayName(deckConfigurationAtLocationLoadName) + deckConfigurationAtLocationFixtureId != null + ? getFixtureDisplayName(deckConfigurationAtLocationFixtureId) : '' const handleUpdateDeck = (): void => { - if (requiredFixture != null) { - updateDeckConfiguration({ - fixtureLocation: cutout, - loadName: requiredFixture, - }) + if (requiredFixtureId != null) { + const newRequiredFixtureDeckConfig = deckConfig.map(fixture => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: requiredFixtureId } + : fixture + ) + + updateDeckConfiguration(newRequiredFixtureDeckConfig) } else { - updateDeckConfiguration({ - fixtureLocation: cutout, - loadName: STANDARD_SLOT_LOAD_NAME, - }) + const isRightCutout = SINGLE_RIGHT_CUTOUTS.includes(cutoutId) + const singleSlotFixture = isRightCutout + ? SINGLE_RIGHT_SLOT_FIXTURE + : SINGLE_LEFT_SLOT_FIXTURE + + const newSingleSlotDeckConfig = deckConfig.map(fixture => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: singleSlotFixture } + : fixture + ) + + updateDeckConfiguration(newSingleSlotDeckConfig) } onCloseClick() } @@ -101,7 +115,7 @@ export const LocationConflictModal = ( i18nKey="deck_conflict_info" values={{ currentFixture: currentFixtureDisplayName, - cutout, + cutout: getCutoutDisplayName(cutoutId), }} components={{ block: , @@ -114,7 +128,9 @@ export const LocationConflictModal = ( fontWeight={TYPOGRAPHY.fontWeightBold} paddingBottom={SPACING.spacing8} > - {t('slot_location', { slotName: cutout })} + {t('slot_location', { + slotName: getCutoutDisplayName(cutoutId), + })} - {requiredFixture != null && - getFixtureDisplayName(requiredFixture)} + {requiredFixtureId != null && + getFixtureDisplayName(requiredFixtureId)} {requiredModule != null && getModuleDisplayName(requiredModule)} @@ -198,7 +214,7 @@ export const LocationConflictModal = ( i18nKey="deck_conflict_info" values={{ currentFixture: currentFixtureDisplayName, - cutout, + cutout: getCutoutDisplayName(cutoutId), }} components={{ block: , @@ -210,7 +226,9 @@ export const LocationConflictModal = ( fontSize={TYPOGRAPHY.fontSizeH4} fontWeight={TYPOGRAPHY.fontWeightBold} > - {t('slot_location', { slotName: cutout })} + {t('slot_location', { + slotName: getCutoutDisplayName(cutoutId), + })} - {requiredFixture != null && - getFixtureDisplayName(requiredFixture)} + {requiredFixtureId != null && + getFixtureDisplayName(requiredFixtureId)} {requiredModule != null && getModuleDisplayName(requiredModule)} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx index c568ef1fe95..dbed3152c1a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx @@ -1,6 +1,9 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client/src/deck_configuration' +import { + useDeckConfigurationQuery, + useUpdateDeckConfigurationMutation, +} from '@opentrons/react-api-client/src/deck_configuration' import { Flex, DIRECTION_COLUMN, @@ -17,26 +20,30 @@ import { Portal } from '../../../../App/portal' import { LegacyModal } from '../../../../molecules/LegacyModal' import { StyledText } from '../../../../atoms/text' -import type { Cutout, FixtureLoadName } from '@opentrons/shared-data' +import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data' interface NotConfiguredModalProps { onCloseClick: () => void - requiredFixture: FixtureLoadName - cutout: Cutout + requiredFixtureId: CutoutFixtureId + cutoutId: CutoutId } export const NotConfiguredModal = ( props: NotConfiguredModalProps ): JSX.Element => { - const { onCloseClick, cutout, requiredFixture } = props + const { onCloseClick, cutoutId, requiredFixtureId } = props const { t, i18n } = useTranslation(['protocol_setup', 'shared']) const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() + const deckConfig = useDeckConfigurationQuery()?.data ?? [] const handleUpdateDeck = (): void => { - updateDeckConfiguration({ - fixtureLocation: cutout, - loadName: requiredFixture, - }) + const newDeckConfig = deckConfig.map(fixture => + fixture.cutoutId === cutoutId + ? { ...fixture, cutoutFixtureId: requiredFixtureId } + : fixture + ) + + updateDeckConfiguration(newDeckConfig) onCloseClick() } @@ -46,7 +53,7 @@ export const NotConfiguredModal = ( title={ {t('add_fixture', { - fixtureName: getFixtureDisplayName(requiredFixture), + fixtureName: getFixtureDisplayName(requiredFixtureId), })} } @@ -64,7 +71,7 @@ export const NotConfiguredModal = ( justifyContent={JUSTIFY_SPACE_BETWEEN} > - {getFixtureDisplayName(requiredFixture)} + {getFixtureDisplayName(requiredFixtureId)} {i18n.format(t('add'), 'capitalize')} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx index 4817fc8ca09..c2c29fdcbea 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import map from 'lodash/map' import { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { @@ -16,32 +15,26 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { - FixtureLoadName, + SINGLE_SLOT_FIXTURES, + getCutoutDisplayName, getFixtureDisplayName, - LoadFixtureRunTimeCommand, } from '@opentrons/shared-data' -import { - useLoadedFixturesConfigStatus, - CONFIGURED, - CONFLICTING, - NOT_CONFIGURED, -} from '../../../../resources/deck_configuration/hooks' import { StyledText } from '../../../../atoms/text' import { StatusLabel } from '../../../../atoms/StatusLabel' import { TertiaryButton } from '../../../../atoms/buttons/TertiaryButton' import { LocationConflictModal } from './LocationConflictModal' import { NotConfiguredModal } from './NotConfiguredModal' import { getFixtureImage } from './utils' +import { DeckFixtureSetupInstructionsModal } from '../../../DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' -import type { LoadedFixturesBySlot } from '@opentrons/api-client' -import type { Cutout } from '@opentrons/shared-data' +import type { CutoutConfigAndCompatibility } from '../../../../resources/deck_configuration/hooks' interface SetupFixtureListProps { - loadedFixturesBySlot: LoadedFixturesBySlot + deckConfigCompatibility: CutoutConfigAndCompatibility[] } export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => { - const { loadedFixturesBySlot } = props + const { deckConfigCompatibility } = props const { t, i18n } = useTranslation('protocol_setup') return ( <> @@ -81,15 +74,11 @@ export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => { gridGap={SPACING.spacing4} marginBottom={SPACING.spacing24} > - {map(loadedFixturesBySlot, ({ params, id }) => { - const { loadName, location } = params + {deckConfigCompatibility.map(cutoutConfigAndCompatibility => { return ( ) })} @@ -98,54 +87,43 @@ export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => { ) } -interface FixtureListItemProps { - loadedFixtures: LoadFixtureRunTimeCommand[] - loadName: FixtureLoadName - cutout: Cutout - commandId: string -} +interface FixtureListItemProps extends CutoutConfigAndCompatibility {} export function FixtureListItem({ - loadedFixtures, - loadName, - cutout, - commandId, + cutoutId, + cutoutFixtureId, + compatibleCutoutFixtureIds, }: FixtureListItemProps): JSX.Element { const { t } = useTranslation('protocol_setup') - const configuration = useLoadedFixturesConfigStatus(loadedFixtures) - const configurationStatus = configuration.find( - config => config.id === commandId - )?.configurationStatus + const isCurrentFixtureCompatible = + cutoutFixtureId != null && + compatibleCutoutFixtureIds.includes(cutoutFixtureId) + const isConflictingFixtureConfigured = + cutoutFixtureId != null && !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) let statusLabel - if ( - configurationStatus === CONFLICTING || - configurationStatus === NOT_CONFIGURED - ) { + if (!isCurrentFixtureCompatible) { statusLabel = ( ) - } else if (configurationStatus === CONFIGURED) { + } else { statusLabel = ( ) - // shouldn't run into this case - } else { - statusLabel = 'status label unknown' } const [ @@ -157,20 +135,30 @@ export function FixtureListItem({ setShowNotConfiguredModal, ] = React.useState(false) + const [ + showSetupInstructionsModal, + setShowSetupInstructionsModal, + ] = React.useState(false) + return ( <> {showNotConfiguredModal ? ( setShowNotConfiguredModal(false)} - cutout={cutout} - requiredFixture={loadName} + cutoutId={cutoutId} + requiredFixtureId={compatibleCutoutFixtureIds[0]} /> ) : null} {showLocationConflictModal ? ( setShowLocationConflictModal(false)} - cutout={cutout} - requiredFixture={loadName} + cutoutId={cutoutId} + requiredFixtureId={compatibleCutoutFixtureIds[0]} + /> + ) : null} + {showSetupInstructionsModal ? ( + ) : null} - + {cutoutFixtureId != null ? ( + + ) : null} - {getFixtureDisplayName(loadName)} + {isCurrentFixtureCompatible + ? getFixtureDisplayName(cutoutFixtureId) + : getFixtureDisplayName(compatibleCutoutFixtureIds?.[0])} console.log('wire this up')} + onClick={() => setShowSetupInstructionsModal(true)} > {t('view_setup_instructions')} @@ -215,7 +214,7 @@ export function FixtureListItem({ - {cutout} + {getCutoutDisplayName(cutoutId)} {statusLabel} - {configurationStatus !== CONFIGURED ? ( + {!isCurrentFixtureCompatible ? ( - configurationStatus === CONFLICTING + isConflictingFixtureConfigured ? setShowLocationConflictModal(true) : setShowNotConfiguredModal(true) } diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx index 0f52f417742..07c943e8341 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx @@ -20,6 +20,8 @@ import { TOOLTIP_LEFT, } from '@opentrons/components' import { + FLEX_ROBOT_TYPE, + getDeckDefFromRobotType, getModuleType, HEATERSHAKER_MODULE_TYPE, HEATERSHAKER_MODULE_V1, @@ -33,6 +35,7 @@ import { TertiaryButton } from '../../../../atoms/buttons' import { StatusLabel } from '../../../../atoms/StatusLabel' import { StyledText } from '../../../../atoms/text' import { Tooltip } from '../../../../atoms/Tooltip' +import { getCutoutIdForSlotName } from '../../../../resources/deck_configuration/utils' import { useChainLiveCommands } from '../../../../resources/runs/hooks' import { ModuleSetupModal } from '../../../ModuleCard/ModuleSetupModal' import { ModuleWizardFlows } from '../../../ModuleWizardFlows' @@ -42,6 +45,7 @@ import { ModuleRenderInfoForProtocol, useIsFlex, useModuleRenderInfoForProtocolById, + useRobot, useUnmatchedModulesForProtocol, useRunCalibrationStatus, } from '../../hooks' @@ -50,7 +54,11 @@ import { MultipleModulesModal } from './MultipleModulesModal' import { UnMatchedModuleWarning } from './UnMatchedModuleWarning' import { getModuleImage } from './utils' -import type { Cutout, ModuleModel, Fixture } from '@opentrons/shared-data' +import type { + CutoutConfig, + DeckDefinition, + ModuleModel, +} from '@opentrons/shared-data' import type { AttachedModule } from '../../../../redux/modules/types' import type { ProtocolCalibrationStatus } from '../../hooks' @@ -63,7 +71,6 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { const { robotName, runId } = props const { t } = useTranslation('protocol_setup') const moduleRenderInfoForProtocolById = useModuleRenderInfoForProtocolById( - robotName, runId ) const { @@ -72,6 +79,8 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { } = useUnmatchedModulesForProtocol(robotName, runId) const isFlex = useIsFlex(robotName) + const { robotModel } = useRobot(robotName) ?? {} + const deckDef = getDeckDefFromRobotType(robotModel ?? FLEX_ROBOT_TYPE) const calibrationStatus = useRunCalibrationStatus(robotName, runId) @@ -188,6 +197,7 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { isFlex={isFlex} calibrationStatus={calibrationStatus} conflictedFixture={conflictedFixture} + deckDef={deckDef} /> ) } @@ -205,7 +215,8 @@ interface ModulesListItemProps { heaterShakerModuleFromProtocol: ModuleRenderInfoForProtocol | null isFlex: boolean calibrationStatus: ProtocolCalibrationStatus - conflictedFixture?: Fixture + deckDef: DeckDefinition + conflictedFixture?: CutoutConfig } export function ModulesListItem({ @@ -217,6 +228,7 @@ export function ModulesListItem({ isFlex, calibrationStatus, conflictedFixture, + deckDef, }: ModulesListItemProps): JSX.Element { const { t } = useTranslation(['protocol_setup', 'module_wizard_flows']) const moduleConnectionStatus = @@ -346,13 +358,16 @@ export function ModulesListItem({ ) } + // convert slot name to cutout id + const cutoutIdForSlotName = getCutoutIdForSlotName(slotName, deckDef) + return ( <> - {showLocationConflictModal ? ( + {showLocationConflictModal && cutoutIdForSlotName != null ? ( setShowLocationConflictModal(false)} // TODO(bh, 2023-10-10): when module caddies are fixtures, narrow slotName to Cutout and remove type assertion - cutout={slotName as Cutout} + cutoutId={cutoutIdForSlotName} requiredModule={moduleModel} /> ) : null} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx index 6d24f7b6eee..0a555112c8c 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx @@ -8,11 +8,11 @@ import { SPACING, } from '@opentrons/components' import { + FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, } from '@opentrons/shared-data' -import { getDeckConfigFromProtocolCommands } from '../../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../../resources/deck_configuration/utils' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { getAttachedProtocolModuleMatches } from '../../../ProtocolSetupModulesAndDeck/utils' import { ModuleInfo } from '../../ModuleInfo' @@ -42,7 +42,7 @@ export const SetupModulesMap = ({ // early return null if no protocol analysis if (protocolAnalysis == null) return null - const robotType = getRobotTypeFromLoadedLabware(protocolAnalysis.labware) + const robotType = protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE const deckDef = getDeckDefFromRobotType(robotType) const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef) @@ -64,7 +64,7 @@ export const SetupModulesMap = ({ ), })) - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( protocolAnalysis.commands ) @@ -77,7 +77,10 @@ export const SetupModulesMap = ({ > ({ + cutoutId, + cutoutFixtureId, + }))} deckLayerBlocklist={getStandardDeckViewLayerBlockList(robotType)} robotType={robotType} labwareLocations={[]} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx index aeca0634478..06d2e1507c7 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx @@ -2,10 +2,8 @@ import * as React from 'react' import { UseQueryResult } from 'react-query' import { renderWithProviders } from '@opentrons/components' import { - DeckConfiguration, - STAGING_AREA_LOAD_NAME, - Fixture, - TRASH_BIN_LOAD_NAME, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, } from '@opentrons/shared-data' import { useDeckConfigurationQuery, @@ -14,6 +12,8 @@ import { import { i18n } from '../../../../../i18n' import { LocationConflictModal } from '../LocationConflictModal' +import type { DeckConfiguration } from '@opentrons/shared-data' + jest.mock('@opentrons/react-api-client/src/deck_configuration') const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction< @@ -24,10 +24,9 @@ const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutatio > const mockFixture = { - fixtureId: 'mockId', - fixtureLocation: 'B3', - loadName: STAGING_AREA_LOAD_NAME, -} as Fixture + cutoutId: 'cutoutB3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, +} const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -41,8 +40,8 @@ describe('LocationConflictModal', () => { beforeEach(() => { props = { onCloseClick: jest.fn(), - cutout: 'B3', - requiredFixture: TRASH_BIN_LOAD_NAME, + cutoutId: 'cutoutB3', + requiredFixtureId: TRASH_BIN_ADAPTER_FIXTURE, } mockUseDeckConfigurationQuery.mockReturnValue({ data: [mockFixture], @@ -57,8 +56,8 @@ describe('LocationConflictModal', () => { getByText('Slot B3') getByText('Protocol specifies') getByText('Currently configured') - getAllByText('Staging Area Slot') - getByText('Trash Bin') + getAllByText('Staging area slot') + getByText('Trash bin') getByRole('button', { name: 'Cancel' }).click() expect(props.onCloseClick).toHaveBeenCalled() getByRole('button', { name: 'Update deck' }).click() @@ -67,7 +66,7 @@ describe('LocationConflictModal', () => { it('should render the modal information for a module fixture conflict', () => { props = { onCloseClick: jest.fn(), - cutout: 'B3', + cutoutId: 'cutoutB3', requiredModule: 'heaterShakerModuleV1', } const { getByText, getByRole } = render(props) @@ -89,8 +88,8 @@ describe('LocationConflictModal', () => { getByText('Slot B3') getByText('Protocol specifies') getByText('Currently configured') - getAllByText('Staging Area Slot') - getByText('Trash Bin') + getAllByText('Staging area slot') + getByText('Trash bin') getByText('Cancel').click() expect(props.onCloseClick).toHaveBeenCalled() getByText('Confirm removal').click() diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx index d18bee369a8..f2d5fb5ca38 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx @@ -1,15 +1,24 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' -import { TRASH_BIN_LOAD_NAME } from '@opentrons/shared-data' -import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client/src/deck_configuration' +import { TRASH_BIN_ADAPTER_FIXTURE } from '@opentrons/shared-data' +import { + useDeckConfigurationQuery, + useUpdateDeckConfigurationMutation, +} from '@opentrons/react-api-client/src/deck_configuration' import { i18n } from '../../../../../i18n' import { NotConfiguredModal } from '../NotConfiguredModal' +import type { UseQueryResult } from 'react-query' +import type { DeckConfiguration } from '@opentrons/shared-data' + jest.mock('@opentrons/react-api-client/src/deck_configuration') const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction< typeof useUpdateDeckConfigurationMutation > +const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFunction< + typeof useDeckConfigurationQuery +> const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -23,20 +32,23 @@ describe('NotConfiguredModal', () => { beforeEach(() => { props = { onCloseClick: jest.fn(), - cutout: 'B3', - requiredFixture: TRASH_BIN_LOAD_NAME, + cutoutId: 'cutoutB3', + requiredFixtureId: TRASH_BIN_ADAPTER_FIXTURE, } mockUseUpdateDeckConfigurationMutation.mockReturnValue({ updateDeckConfiguration: mockUpdate, } as any) + mockUseDeckConfigurationQuery.mockReturnValue(({ + data: [], + } as unknown) as UseQueryResult) }) it('renders the correct text and button works as expected', () => { const { getByText, getByRole } = render(props) - getByText('Add Trash Bin to deck configuration') + getByText('Add Trash bin to deck configuration') getByText( 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' ) - getByText('Trash Bin') + getByText('Trash bin') getByRole('button', { name: 'Add' }).click() expect(mockUpdate).toHaveBeenCalled() }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx index 8da24a61675..d9245a370e4 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx @@ -1,47 +1,41 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' -import { - LoadFixtureRunTimeCommand, - WASTE_CHUTE_LOAD_NAME, - WASTE_CHUTE_SLOT, -} from '@opentrons/shared-data' +import { STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE } from '@opentrons/shared-data' import { i18n } from '../../../../../i18n' -import { useLoadedFixturesConfigStatus } from '../../../../../resources/deck_configuration/hooks' import { SetupFixtureList } from '../SetupFixtureList' import { NotConfiguredModal } from '../NotConfiguredModal' import { LocationConflictModal } from '../LocationConflictModal' -import type { LoadedFixturesBySlot } from '@opentrons/api-client' +import { DeckFixtureSetupInstructionsModal } from '../../../../DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' + +import type { CutoutConfigAndCompatibility } from '../../../../../resources/deck_configuration/hooks' jest.mock('../../../../../resources/deck_configuration/hooks') jest.mock('../LocationConflictModal') jest.mock('../NotConfiguredModal') +jest.mock( + '../../../../DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' +) -const mockUseLoadedFixturesConfigStatus = useLoadedFixturesConfigStatus as jest.MockedFunction< - typeof useLoadedFixturesConfigStatus -> const mockLocationConflictModal = LocationConflictModal as jest.MockedFunction< typeof LocationConflictModal > const mockNotConfiguredModal = NotConfiguredModal as jest.MockedFunction< typeof NotConfiguredModal > -const mockLoadedFixture = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'D3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', -} as LoadFixtureRunTimeCommand +const mockDeckFixtureSetupInstructionsModal = DeckFixtureSetupInstructionsModal as jest.MockedFunction< + typeof DeckFixtureSetupInstructionsModal +> -const mockLoadedFixturesBySlot: LoadedFixturesBySlot = { - D3: mockLoadedFixture, -} +const mockDeckConfigCompatibility: CutoutConfigAndCompatibility[] = [ + { + cutoutId: 'cutoutD3', + cutoutFixtureId: STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + requiredAddressableAreas: ['D4'], + compatibleCutoutFixtureIds: [ + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + ], + }, +] const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -53,18 +47,15 @@ describe('SetupFixtureList', () => { let props: React.ComponentProps beforeEach(() => { props = { - loadedFixturesBySlot: mockLoadedFixturesBySlot, + deckConfigCompatibility: mockDeckConfigCompatibility, } - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { - ...mockLoadedFixture, - configurationStatus: 'configured', - }, - ]) mockLocationConflictModal.mockReturnValue(
mock location conflict modal
) mockNotConfiguredModal.mockReturnValue(
mock not configured modal
) + mockDeckFixtureSetupInstructionsModal.mockReturnValue( +
mock DeckFixtureSetupInstructionsModal
+ ) }) it('should render the headers and a fixture with configured status', () => { @@ -72,33 +63,29 @@ describe('SetupFixtureList', () => { getByText('Fixture') getByText('Location') getByText('Status') - getByText('Waste Chute') + getByText('Waste chute with staging area slot') getByRole('button', { name: 'View setup instructions' }) - getByText(WASTE_CHUTE_SLOT) + getByText('D3') getByText('Configured') }) - it('should render the headers and a fixture with conflicted status', () => { - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { - ...mockLoadedFixture, - configurationStatus: 'conflicting', - }, - ]) - const { getByText, getByRole } = render(props)[0] - getByText('Location conflict') - getByRole('button', { name: 'Update deck' }).click() - getByText('mock location conflict modal') - }) - it('should render the headers and a fixture with not configured status and button', () => { - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { - ...mockLoadedFixture, - configurationStatus: 'not configured', - }, - ]) + + it('should render the mock setup instructions modal, when clicking view setup instructions', () => { const { getByText, getByRole } = render(props)[0] - getByText('Not configured') - getByRole('button', { name: 'Update deck' }).click() - getByText('mock not configured modal') + getByRole('button', { name: 'View setup instructions' }).click() + getByText('mock DeckFixtureSetupInstructionsModal') }) + + // TODO(bh, 2023-11-14): implement test cases when example JSON protocol fixtures exist + // it('should render the headers and a fixture with conflicted status', () => { + // const { getByText, getByRole } = render(props)[0] + // getByText('Location conflict') + // getByRole('button', { name: 'Update deck' }).click() + // getByText('mock location conflict modal') + // }) + // it('should render the headers and a fixture with not configured status and button', () => { + // const { getByText, getByRole } = render(props)[0] + // getByText('Not configured') + // getByRole('button', { name: 'Update deck' }).click() + // getByText('mock not configured modal') + // }) }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx index fa44f9b124c..af0077600aa 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesAndDeck.test.tsx @@ -2,8 +2,9 @@ import * as React from 'react' import { fireEvent } from '@testing-library/react' import { when } from 'jest-when' import { renderWithProviders } from '@opentrons/components' -import { WASTE_CHUTE_LOAD_NAME } from '@opentrons/shared-data' import { i18n } from '../../../../../i18n' +import { mockTemperatureModule } from '../../../../../redux/modules/__fixtures__' +import { getRequiredDeckConfig } from '../../../../../resources/deck_configuration/utils' import { useIsFlex, useRunHasStarted, @@ -14,30 +15,13 @@ import { SetupModuleAndDeck } from '../index' import { SetupModulesList } from '../SetupModulesList' import { SetupModulesMap } from '../SetupModulesMap' import { SetupFixtureList } from '../SetupFixtureList' -import { mockTemperatureModule } from '../../../../../redux/modules/__fixtures__' -import { LoadedFixturesBySlot } from '@opentrons/api-client' - -const mockLoadedFixturesBySlot: LoadedFixturesBySlot = { - D3: { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'D3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - }, -} jest.mock('../../../hooks') jest.mock('../SetupModulesList') jest.mock('../SetupModulesMap') jest.mock('../SetupFixtureList') jest.mock('../../../../../redux/config') +jest.mock('../../../../../resources/deck_configuration/utils') const mockUseIsFlex = useIsFlex as jest.MockedFunction const mockUseRunHasStarted = useRunHasStarted as jest.MockedFunction< @@ -58,6 +42,9 @@ const mockSetupFixtureList = SetupFixtureList as jest.MockedFunction< const mockSetupModulesMap = SetupModulesMap as jest.MockedFunction< typeof SetupModulesMap > +const mockGetRequiredDeckConfig = getRequiredDeckConfig as jest.MockedFunction< + typeof getRequiredDeckConfig +> const MOCK_ROBOT_NAME = 'otie' const MOCK_RUN_ID = '1' @@ -75,7 +62,7 @@ describe('SetupModuleAndDeck', () => { runId: MOCK_RUN_ID, expandLabwarePositionCheckStep: () => jest.fn(), hasModules: true, - loadedFixturesBySlot: {}, + commands: [], } mockSetupFixtureList.mockReturnValue(
Mock setup fixture list
) mockSetupModulesList.mockReturnValue(
Mock setup modules list
) @@ -91,6 +78,7 @@ describe('SetupModuleAndDeck', () => { .calledWith(MOCK_ROBOT_NAME, MOCK_RUN_ID) .mockReturnValue({ complete: true }) when(mockUseIsFlex).calledWith(MOCK_ROBOT_NAME).mockReturnValue(false) + when(mockGetRequiredDeckConfig).mockReturnValue([]) }) it('renders the list and map view buttons', () => { @@ -141,7 +129,13 @@ describe('SetupModuleAndDeck', () => { it('should render the SetupModulesList and SetupFixtureList component when clicking List View for Flex', () => { when(mockUseIsFlex).calledWith(MOCK_ROBOT_NAME).mockReturnValue(true) - props.loadedFixturesBySlot = mockLoadedFixturesBySlot + when(mockGetRequiredDeckConfig).mockReturnValue([ + { + cutoutId: 'cutoutA1', + cutoutFixtureId: 'trashBinAdapter', + requiredAddressableAreas: ['movableTrashA1'], + }, + ]) const { getByRole, getByText } = render(props) const button = getByRole('button', { name: 'List View' }) fireEvent.click(button) @@ -149,6 +143,15 @@ describe('SetupModuleAndDeck', () => { getByText('Mock setup fixture list') }) + it('should not render the SetupFixtureList component when there are no required fixtures', () => { + when(mockUseIsFlex).calledWith(MOCK_ROBOT_NAME).mockReturnValue(true) + const { getByRole, getByText, queryByText } = render(props) + const button = getByRole('button', { name: 'List View' }) + fireEvent.click(button) + getByText('Mock setup modules list') + expect(queryByText('Mock setup fixture list')).toBeNull() + }) + it('should render the SetupModulesMap component when clicking Map View', () => { const { getByRole, getByText } = render(props) const button = getByRole('button', { name: 'Map View' }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx index 4e2fc01c73c..7dc10b9d8ef 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { when, resetAllWhenMocks } from 'jest-when' import { fireEvent, waitFor } from '@testing-library/react' import { renderWithProviders } from '@opentrons/components' +import { STAGING_AREA_RIGHT_SLOT_FIXTURE } from '@opentrons/shared-data' import { i18n } from '../../../../../i18n' import { mockMagneticModule as mockMagneticModuleFixture, @@ -27,11 +28,7 @@ import { UnMatchedModuleWarning } from '../UnMatchedModuleWarning' import { SetupModulesList } from '../SetupModulesList' import { LocationConflictModal } from '../LocationConflictModal' -import { - ModuleModel, - ModuleType, - STAGING_AREA_LOAD_NAME, -} from '@opentrons/shared-data' +import type { ModuleModel, ModuleType } from '@opentrons/shared-data' jest.mock('@opentrons/react-api-client') jest.mock('../../../hooks') @@ -156,7 +153,7 @@ describe('SetupModulesList', () => { it('should render the list view headers', () => { when(mockUseRunHasStarted).calledWith(RUN_ID).mockReturnValue(false) when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) + .calledWith(RUN_ID) .mockReturnValue({}) const { getByText } = render(props) getByText('Module') @@ -352,7 +349,7 @@ describe('SetupModulesList', () => { const dupModPort = 10 const dupModHub = 2 when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) + .calledWith(RUN_ID) .mockReturnValue({ [mockMagneticModule.moduleId]: { moduleId: mockMagneticModule.moduleId, @@ -446,7 +443,7 @@ describe('SetupModulesList', () => { fireEvent.click(moduleSetup) getByText('mockModuleSetupModal') }) - it('shoulde render a magnetic block with a conflicted fixture', () => { + it('should render a magnetic block with a conflicted fixture', () => { when(mockUseIsFlex).calledWith(ROBOT_NAME).mockReturnValue(true) mockUseModuleRenderInfoForProtocolById.mockReturnValue({ [mockMagneticBlock.id]: { @@ -463,12 +460,11 @@ describe('SetupModulesList', () => { nestedLabwareDef: null, nestedLabwareId: null, protocolLoadOrder: 0, - slotName: '1', + slotName: 'B3', attachedModuleMatch: null, conflictedFixture: { - fixtureId: 'mockId', - fixtureLocation: '1', - loadName: STAGING_AREA_LOAD_NAME, + cutoutId: 'cutoutB3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, }, }, } as any) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx index 429ba4d9145..f2e1e01e4a3 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesMap.test.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import '@testing-library/jest-dom' import { when, resetAllWhenMocks } from 'jest-when' import { StaticRouter } from 'react-router-dom' +import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { renderWithProviders, @@ -113,6 +114,7 @@ describe('SetupModulesMap', () => { .mockReturnValue(({ commands: [], labware: [], + robotType: OT2_ROBOT_TYPE, } as unknown) as CompletedProtocolAnalysis) when(mockGetAttachedProtocolModuleMatches).mockReturnValue([]) }) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts index 96e1470f78f..2388de2b936 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/utils.test.ts @@ -44,15 +44,15 @@ describe('getModuleImage', () => { describe('getFixtureImage', () => { it('should render the staging area image', () => { - const result = getFixtureImage('stagingArea') + const result = getFixtureImage('stagingAreaRightSlot') expect(result).toEqual('staging_area_slot.png') }) it('should render the waste chute image', () => { - const result = getFixtureImage('wasteChute') + const result = getFixtureImage('wasteChuteRightAdapterNoCover') expect(result).toEqual('waste_chute.png') }) it('should render the trash binimage', () => { - const result = getFixtureImage('trashBin') + const result = getFixtureImage('trashBinAdapter') expect(result).toEqual('flex_trash_bin.png') }) // TODO(jr, 10/17/23): add rest of the test cases when we add the assets diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx index 3ef1a8c7603..b914773b2cb 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx @@ -8,33 +8,37 @@ import { useHoverTooltip, PrimaryButton, } from '@opentrons/components' + import { useToggleGroup } from '../../../../molecules/ToggleGroup/useToggleGroup' +import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks' +import { getRequiredDeckConfig } from '../../../../resources/deck_configuration/utils' import { Tooltip } from '../../../../atoms/Tooltip' import { - useIsFlex, useRunHasStarted, useUnmatchedModulesForProtocol, useModuleCalibrationStatus, + useRobotType, } from '../../hooks' import { SetupModulesMap } from './SetupModulesMap' import { SetupModulesList } from './SetupModulesList' import { SetupFixtureList } from './SetupFixtureList' -import type { LoadedFixturesBySlot } from '@opentrons/api-client' + +import type { RunTimeCommand } from '@opentrons/shared-data' interface SetupModuleAndDeckProps { expandLabwarePositionCheckStep: () => void robotName: string runId: string - loadedFixturesBySlot: LoadedFixturesBySlot hasModules: boolean + commands: RunTimeCommand[] } export const SetupModuleAndDeck = ({ expandLabwarePositionCheckStep, robotName, runId, - loadedFixturesBySlot, hasModules, + commands, }: SetupModuleAndDeckProps): JSX.Element => { const { t } = useTranslation('protocol_setup') const [selectedValue, toggleGroup] = useToggleGroup( @@ -42,12 +46,20 @@ export const SetupModuleAndDeck = ({ t('map_view') ) - const isFlex = useIsFlex(robotName) + const robotType = useRobotType(robotName) const { missingModuleIds } = useUnmatchedModulesForProtocol(robotName, runId) const runHasStarted = useRunHasStarted(runId) const [targetProps, tooltipProps] = useHoverTooltip() const moduleCalibrationStatus = useModuleCalibrationStatus(robotName, runId) + const deckConfigCompatibility = useDeckConfigurationCompatibility( + robotType, + commands + ) + + const requiredDeckConfigCompatibility = getRequiredDeckConfig( + deckConfigCompatibility + ) return ( <> @@ -58,8 +70,10 @@ export const SetupModuleAndDeck = ({ {hasModules ? ( ) : null} - {Object.keys(loadedFixturesBySlot).length > 0 && isFlex ? ( - + {requiredDeckConfigCompatibility.length > 0 ? ( + ) : null} ) : ( diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts index 171a26199f4..ee5b72efee5 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/utils.ts @@ -1,3 +1,10 @@ +import { + SINGLE_SLOT_FIXTURES, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_FIXTURES, +} from '@opentrons/shared-data' + import magneticModule from '../../../../assets/images/magnetic_module_gen_2_transparent.png' import temperatureModule from '../../../../assets/images/temp_deck_gen_2_transparent.png' import thermoModuleGen1 from '../../../../assets/images/thermocycler_closed.png' @@ -9,7 +16,13 @@ import stagingArea from '../../../../assets/images/staging_area_slot.png' import wasteChute from '../../../../assets/images/waste_chute.png' // TODO(jr, 10/17/23): figure out if we need this asset, I'm stubbing it in for now // import wasteChuteStagingArea from '../../../../assets/images/waste_chute_with_staging_area.png' -import type { FixtureLoadName, ModuleModel } from '@opentrons/shared-data' + +import type { + CutoutFixtureId, + ModuleModel, + SingleSlotCutoutFixtureId, + WasteChuteCutoutFixtureId, +} from '@opentrons/shared-data' export function getModuleImage(model: ModuleModel): string { switch (model) { @@ -33,21 +46,21 @@ export function getModuleImage(model: ModuleModel): string { } // TODO(jr, 10/4/23): add correct assets for trashBin, standardSlot, wasteChuteAndStagingArea -export function getFixtureImage(fixture: FixtureLoadName): string { - switch (fixture) { - case 'stagingArea': { - return stagingArea - } - case 'wasteChute': { - return wasteChute - } - case 'standardSlot': { - return stagingArea - } - case 'trashBin': { - return trashBin - } - default: - return 'Error: unknown fixture' +export function getFixtureImage(fixture: CutoutFixtureId): string { + if (fixture === STAGING_AREA_RIGHT_SLOT_FIXTURE) { + return stagingArea + } else if ( + WASTE_CHUTE_FIXTURES.includes(fixture as WasteChuteCutoutFixtureId) + ) { + return wasteChute + } else if ( + // TODO(bh, 2023-11-13): this asset probably won't exist + SINGLE_SLOT_FIXTURES.includes(fixture as SingleSlotCutoutFixtureId) + ) { + return stagingArea + } else if (fixture === TRASH_BIN_ADAPTER_FIXTURE) { + return trashBin + } else { + return 'Error: unknown fixture' } } diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx index b4d2be4862f..877ae596328 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx @@ -39,7 +39,6 @@ const mockUseInstrumentsQuery = useInstrumentsQuery as jest.MockedFunction< const _fixtureAnalysis = (fixtureAnalysis as unknown) as CompletedProtocolAnalysis -const ROBOT_NAME = 'otie' const RUN_ID = 'test123' const mockTempMod = { @@ -97,7 +96,7 @@ describe('ProtocolRunModuleControls', () => { it('renders a magnetic module card', () => { when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) + .calledWith(RUN_ID) .mockReturnValue({ [mockMagMod.moduleId]: { moduleId: 'magModModuleId', @@ -122,7 +121,7 @@ describe('ProtocolRunModuleControls', () => { it('renders a temperature module card', () => { when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) + .calledWith(RUN_ID) .mockReturnValue({ [mockTempMod.moduleId]: { moduleId: 'temperatureModuleId', @@ -149,7 +148,7 @@ describe('ProtocolRunModuleControls', () => { it('renders a thermocycler module card', () => { when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) + .calledWith(RUN_ID) .mockReturnValue({ [mockTCModule.moduleId]: { moduleId: mockTCModule.moduleId, @@ -178,7 +177,7 @@ describe('ProtocolRunModuleControls', () => { it('renders a heater-shaker module card', () => { when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) + .calledWith(RUN_ID) .mockReturnValue({ [mockHeaterShakerDef.moduleId]: { moduleId: 'heaterShakerModuleId', @@ -206,7 +205,7 @@ describe('ProtocolRunModuleControls', () => { it('renders correct text when module is not attached but required for protocol', () => { when(mockUseModuleRenderInfoForProtocolById) - .calledWith(ROBOT_NAME, RUN_ID) + .calledWith(RUN_ID) .mockReturnValue({ [mockHeaterShakerDef.moduleId]: { moduleId: 'heaterShakerModuleId', diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx index 63aafc70d1d..067e285a04c 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx @@ -12,6 +12,10 @@ import withModulesProtocol from '@opentrons/shared-data/protocol/fixtures/4/test import { i18n } from '../../../../i18n' import { mockConnectedRobot } from '../../../../redux/discovery/__fixtures__' +import { + getRequiredDeckConfig, + getSimplestDeckConfigForProtocolCommands, +} from '../../../../resources/deck_configuration/utils' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useIsFlex, @@ -39,6 +43,7 @@ jest.mock('../EmptySetupStep') jest.mock('../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') jest.mock('@opentrons/shared-data/js/helpers/parseProtocolData') jest.mock('../../../../redux/config') +jest.mock('../../../../resources/deck_configuration/utils') const mockUseIsFlex = useIsFlex as jest.MockedFunction const mockUseMostRecentCompletedAnalysis = useMostRecentCompletedAnalysis as jest.MockedFunction< @@ -78,6 +83,13 @@ const mockSetupLiquids = SetupLiquids as jest.MockedFunction< const mockEmptySetupStep = EmptySetupStep as jest.MockedFunction< typeof EmptySetupStep > +const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction< + typeof getSimplestDeckConfigForProtocolCommands +> +const mockGetRequiredDeckConfig = getRequiredDeckConfig as jest.MockedFunction< + typeof getRequiredDeckConfig +> + const ROBOT_NAME = 'otie' const RUN_ID = '1' const MOCK_ROTOCOL_LIQUID_KEY = { liquids: [] } @@ -140,6 +152,8 @@ describe('ProtocolRunSetup', () => { when(mockSetupModuleAndDeck).mockReturnValue(
Mock SetupModules
) when(mockSetupLiquids).mockReturnValue(
Mock SetupLiquids
) when(mockEmptySetupStep).mockReturnValue(
Mock EmptySetupStep
) + when(mockGetSimplestDeckConfigForProtocolCommands).mockReturnValue([]) + when(mockGetRequiredDeckConfig).mockReturnValue([]) }) afterEach(() => { resetAllWhenMocks() diff --git a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts index f865b679b0c..359e4faad3f 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts +++ b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getLabwareRenderInfo.test.ts @@ -1,5 +1,5 @@ import _protocolWithMagTempTC from '@opentrons/shared-data/protocol/fixtures/6/transferSettings.json' -import _standardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot2_standard.json' +import _standardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot2_standard.json' import { getLabwareRenderInfo } from '../getLabwareRenderInfo' import type { CompletedProtocolAnalysis, diff --git a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts index 7b7c2362eb0..f23a369d359 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts +++ b/app/src/organisms/Devices/ProtocolRun/utils/__tests__/getProtocolModulesInfo.test.ts @@ -1,6 +1,6 @@ import _protocolWithMagTempTC from '@opentrons/shared-data/protocol/fixtures/6/transferSettings.json' import _protocolWithMultipleTemps from '@opentrons/shared-data/protocol/fixtures/6/multipleTempModules.json' -import _standardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot2_standard.json' +import _standardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot2_standard.json' import { getProtocolModulesInfo } from '../getProtocolModulesInfo' import { getModuleDef2, diff --git a/app/src/organisms/Devices/ProtocolRun/utils/getLabwareOffsetLocation.ts b/app/src/organisms/Devices/ProtocolRun/utils/getLabwareOffsetLocation.ts index e831be067d9..287c5ef1338 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/getLabwareOffsetLocation.ts +++ b/app/src/organisms/Devices/ProtocolRun/utils/getLabwareOffsetLocation.ts @@ -39,6 +39,11 @@ export const getLabwareOffsetLocation = ( slotName: adapter.location.slotName, definitionUri: adapter.definitionUri, } + } else if ('addressableAreaName' in adapter.location) { + return { + slotName: adapter.location.addressableAreaName, + definitionUri: adapter.definitionUri, + } } else if ('moduleId' in adapter.location) { const moduleIdUnderAdapter = adapter.location.moduleId const moduleModel = modules.find( @@ -52,7 +57,12 @@ export const getLabwareOffsetLocation = ( return { slotName, moduleModel, definitionUri: adapter.definitionUri } } } else { - return { slotName: labwareLocation.slotName } + return { + slotName: + 'addressableAreaName' in labwareLocation + ? labwareLocation.addressableAreaName + : labwareLocation.slotName, + } } return null } diff --git a/app/src/organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo.ts b/app/src/organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo.ts index 86ae0ec6146..5d750e6d7e8 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo.ts +++ b/app/src/organisms/Devices/ProtocolRun/utils/getLabwareRenderInfo.ts @@ -1,4 +1,4 @@ -import { getSlotHasMatingSurfaceUnitVector } from '@opentrons/shared-data' +import { getPositionFromSlotId } from '@opentrons/shared-data' import type { CompletedProtocolAnalysis, DeckDefinition, @@ -7,32 +7,6 @@ import type { ProtocolAnalysisOutput, } from '@opentrons/shared-data' -const getSlotPosition = ( - deckDef: DeckDefinition, - slotName: string -): [number, number, number] => { - let x = 0 - let y = 0 - let z = 0 - const slotPosition = deckDef.locations.orderedSlots.find( - orderedSlot => orderedSlot.id === slotName - )?.position - - if (slotPosition == null) { - console.error( - `expected to find a slot position for slot ${slotName} in ${String( - deckDef.metadata.displayName - )}, but could not` - ) - } else { - x = slotPosition[0] - y = slotPosition[1] - z = slotPosition[2] - } - - return [x, y, z] -} - export interface LabwareRenderInfoById { [labwareId: string]: { x: number @@ -74,17 +48,24 @@ export const getLabwareRenderInfo = ( )} but could not` ) } - // TODO(bh, 2023-10-19): convert this to deck definition v4 addressableAreas - const slotName = location.slotName.toString() - // TODO(bh, 2023-10-19): remove slotPosition when render info no longer relies on directly - const slotPosition = getSlotPosition(deckDef, slotName) - const slotHasMatingSurfaceVector = getSlotHasMatingSurfaceUnitVector( - deckDef, - slotName + const slotName = + 'addressableAreaName' in location + ? location.addressableAreaName + : location.slotName + const slotPosition = getPositionFromSlotId(slotName, deckDef) + + if (slotPosition == null) { + console.warn( + `expected to find a position for slot ${slotName} in the standard deck definition, but could not` + ) + return acc + } + const isSlot = deckDef.locations.addressableAreas.some( + aa => aa.id === slotName && aa.areaType === 'slot' ) - return slotHasMatingSurfaceVector + return isSlot ? { ...acc, [labwareId]: { @@ -96,5 +77,5 @@ export const getLabwareRenderInfo = ( slotName, }, } - : { ...acc } + : acc }, {}) diff --git a/app/src/organisms/Devices/ProtocolRun/utils/getLocationInfoNames.ts b/app/src/organisms/Devices/ProtocolRun/utils/getLocationInfoNames.ts index ee49db3573e..594d5d37f97 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/getLocationInfoNames.ts +++ b/app/src/organisms/Devices/ProtocolRun/utils/getLocationInfoNames.ts @@ -46,6 +46,8 @@ export function getLocationInfoNames( return { slotName: 'Off deck', labwareName } } else if ('slotName' in labwareLocation) { return { slotName: labwareLocation.slotName, labwareName } + } else if ('addressableAreaName' in labwareLocation) { + return { slotName: labwareLocation.addressableAreaName, labwareName } } else if ('moduleId' in labwareLocation) { const loadModuleCommandUnderLabware = loadModuleCommands?.find( command => command.result?.moduleId === labwareLocation.moduleId diff --git a/app/src/organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo.ts b/app/src/organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo.ts index 0b0f3bc23b0..37a5ca93a26 100644 --- a/app/src/organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo.ts +++ b/app/src/organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo.ts @@ -2,6 +2,7 @@ import { SPAN7_8_10_11_SLOT, getModuleDef2, getLoadedLabwareDefinitionsByUri, + getPositionFromSlotId, } from '@opentrons/shared-data' import { getModuleInitialLoadInfo } from './getModuleInitialLoadInfo' import type { @@ -68,13 +69,14 @@ export const getProtocolModulesInfo = ( if (slotName === SPAN7_8_10_11_SLOT) { slotName = '7' } - const slotPosition = - deckDef.locations.orderedSlots.find(slot => slot.id === slotName) - ?.position ?? [] - if (slotPosition.length === 0) { + + const slotPosition = getPositionFromSlotId(slotName, deckDef) + + if (slotPosition == null) { console.warn( - `expected to find a slot position for slot ${slotName} in the standard OT-2 deck definition, but could not` + `expected to find a position for slot ${slotName} in the standard deck definition, but could not` ) + return acc } return [ ...acc, diff --git a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx index 82b35f2b04a..1a043507912 100644 --- a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/ResultModal.tsx @@ -1,25 +1,28 @@ import * as React from 'react' import { AlertModal, SpinnerModal } from '@opentrons/components' + +import * as Copy from './i18n' import { ErrorModal } from '../../../../molecules/modals' import { DISCONNECT } from './constants' -import * as Copy from './i18n' +import { PENDING, FAILURE } from '../../../../redux/robot-api' import type { NetworkChangeType } from './types' +import type { RequestStatus } from '../../../../redux/robot-api/types' export interface ResultModalProps { type: NetworkChangeType ssid: string | null - isPending: boolean + requestStatus: RequestStatus error: { message?: string; [key: string]: unknown } | null onClose: () => unknown } export const ResultModal = (props: ResultModalProps): JSX.Element => { - const { type, ssid, isPending, error, onClose } = props + const { type, ssid, requestStatus, error, onClose } = props const isDisconnect = type === DISCONNECT - if (isPending) { + if (requestStatus === PENDING) { const message = isDisconnect ? Copy.DISCONNECTING_FROM_NETWORK(ssid) : Copy.CONNECTING_TO_NETWORK(ssid) @@ -27,7 +30,7 @@ export const ResultModal = (props: ResultModalProps): JSX.Element => { return } - if (error) { + if (error || requestStatus === FAILURE) { const heading = isDisconnect ? Copy.UNABLE_TO_DISCONNECT : Copy.UNABLE_TO_CONNECT @@ -38,11 +41,15 @@ export const ResultModal = (props: ResultModalProps): JSX.Element => { const retryMessage = !isDisconnect ? ` ${Copy.CHECK_YOUR_CREDENTIALS}.` : '' + const placeholderError = { + message: `Likely incorrect network password. ${Copy.CHECK_YOUR_CREDENTIALS}.`, + } + return ( ) diff --git a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/ResultModal.test.tsx b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/ResultModal.test.tsx index 3cf51d805b4..00f7e9ce389 100644 --- a/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/ResultModal.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/ConnectNetwork/__tests__/ResultModal.test.tsx @@ -5,6 +5,7 @@ import { AlertModal, SpinnerModal } from '@opentrons/components' import { ErrorModal } from '../../../../../molecules/modals' import { ResultModal } from '../ResultModal' import { DISCONNECT, CONNECT, JOIN_OTHER } from '../constants' +import { PENDING, FAILURE, SUCCESS } from '../../../../../redux/robot-api' import type { ShallowWrapper } from 'enzyme' import type { ResultModalProps } from '../ResultModal' @@ -31,7 +32,7 @@ describe("SelectNetwork's ResultModal", () => { type, ssid, error: null, - isPending: true, + requestStatus: PENDING, onClose: handleClose, }} /> @@ -99,7 +100,7 @@ describe("SelectNetwork's ResultModal", () => { type, ssid, error: null, - isPending: false, + requestStatus: SUCCESS, onClose: handleClose, }} /> @@ -188,7 +189,7 @@ describe("SelectNetwork's ResultModal", () => { type, ssid, error, - isPending: false, + requestStatus: FAILURE, onClose: handleClose, }} /> @@ -249,5 +250,41 @@ describe("SelectNetwork's ResultModal", () => { expect(alert.prop('close')).toEqual(handleClose) expect(alert.prop('error')).toEqual(error) }) + + it('displays an ErrorModal with appropriate failure message if the status is failure and no error message is given', () => { + const render: ( + type: ResultModalProps['type'], + ssid?: ResultModalProps['ssid'] + ) => ShallowWrapper> = ( + type, + ssid = mockSsid + ) => { + return shallow( + + ) + } + + const wrapper = render(JOIN_OTHER, null) + const alert = wrapper.find(ErrorModal) + + expect(alert).toHaveLength(1) + expect(alert.prop('heading')).toEqual('Unable to connect to Wi-Fi') + expect(alert.prop('description')).toEqual( + expect.stringContaining('unable to connect to Wi-Fi') + ) + expect(alert.prop('close')).toEqual(handleClose) + expect(alert.prop('error')).toEqual({ + message: + 'Likely incorrect network password. Please double-check your network credentials.', + }) + }) }) }) diff --git a/app/src/organisms/Devices/RobotSettings/SelectNetwork.tsx b/app/src/organisms/Devices/RobotSettings/SelectNetwork.tsx index b3151d0418e..600b4c186d6 100644 --- a/app/src/organisms/Devices/RobotSettings/SelectNetwork.tsx +++ b/app/src/organisms/Devices/RobotSettings/SelectNetwork.tsx @@ -104,7 +104,7 @@ export const SelectNetwork = ({ { if (closeUpdateBuildroot != null) closeUpdateBuildroot() } - const { error } = session || { error: null } - const { updateStep, progressPercent } = useRobotUpdateInfo(session) + + const { updateStep, progressPercent } = useRobotUpdateInfo(robotName, session) + + let { error } = session || { error: null } + const downloadError = useSelector((state: State) => + getRobotUpdateDownloadError(state, robotName) + ) + if (error == null && downloadError != null) error = downloadError + useStatusBarAnimation(error != null) useCleanupRobotUpdateSessionOnDismount() @@ -89,11 +98,27 @@ export function RobotUpdateProgressModal({ progressPercent ) - let modalBodyText = t('installing_update') + let modalBodyText = '' let subProgressBarText = t('do_not_turn_off') - if (updateStep === 'restart') modalBodyText = t('restarting_robot') - if (updateStep === 'restart' && letUserExitUpdate) { - subProgressBarText = t('restart_taking_too_long', { robotName }) + switch (updateStep) { + case 'initial': + case 'error': + modalBodyText = '' + break + case 'download': + modalBodyText = t('downloading_update') + break + case 'install': + modalBodyText = t('installing_update') + break + case 'restart': + modalBodyText = t('restarting_robot') + if (letUserExitUpdate) { + subProgressBarText = t('restart_taking_too_long', { robotName }) + } + break + default: + modalBodyText = t('installing_update') } return ( @@ -209,7 +234,7 @@ function SuccessOrError({ errorMessage }: SuccessOrErrorProps): JSX.Element { export const TIME_BEFORE_ALLOWING_EXIT_MS = 600000 // 10 mins function useAllowExitIfUpdateStalled( - updateStep: UpdateStep, + updateStep: UpdateStep | null, progressPercent: number ): boolean { const [letUserExitUpdate, setLetUserExitUpdate] = React.useState( diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx index 674c30a480c..3762cdd1955 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx @@ -7,7 +7,8 @@ import { useHoverTooltip, ALIGN_CENTER, DIRECTION_COLUMN, - JUSTIFY_FLEX_END, + JUSTIFY_SPACE_BETWEEN, + JUSTIFY_SPACE_AROUND, SPACING, Flex, NewPrimaryBtn, @@ -22,7 +23,9 @@ import { UPGRADE, REINSTALL, DOWNGRADE, + getRobotUpdateVersion, } from '../../../../redux/robot-update' +import { ExternalLink } from '../../../../atoms/Link/ExternalLink' import { ReleaseNotes } from '../../../../molecules/ReleaseNotes' import { useIsRobotBusy } from '../../hooks' import { Tooltip } from '../../../../atoms/Tooltip' @@ -33,6 +36,9 @@ import { useDispatchStartRobotUpdate } from '../../../../redux/robot-update/hook import type { State, Dispatch } from '../../../../redux/types' import type { RobotSystemType } from '../../../../redux/robot-update/types' +export const RELEASE_NOTES_URL_BASE = + 'https://github.com/Opentrons/opentrons/releases/tag/v' + const UpdateAppBanner = styled(Banner)` border: none; ` @@ -73,6 +79,10 @@ export function UpdateRobotModal({ return getRobotUpdateDisplayInfo(state, robotName) }) const dispatchStartRobotUpdate = useDispatchStartRobotUpdate() + const robotUpdateVersion = useSelector((state: State) => { + return getRobotUpdateVersion(state, robotName) ?? '' + }) + const isRobotBusy = useIsRobotBusy() const updateDisabled = updateFromFileDisabledReason !== null || isRobotBusy @@ -98,28 +108,40 @@ export function UpdateRobotModal({ } const robotUpdateFooter = ( - - + - {updateType === UPGRADE ? t('remind_me_later') : t('not_now')} - - dispatchStartRobotUpdate(robotName)} - marginRight={SPACING.spacing12} - css={FOOTER_BUTTON_STYLE} - disabled={updateDisabled} - {...updateButtonProps} - > - {t('update_robot_now')} - - {updateDisabled && ( - - {disabledReason} - - )} + {t('release_notes')} + + + + {updateType === UPGRADE ? t('remind_me_later') : t('not_now')} + + dispatchStartRobotUpdate(robotName)} + marginRight={SPACING.spacing12} + css={FOOTER_BUTTON_STYLE} + disabled={updateDisabled} + {...updateButtonProps} + > + {t('update_robot_now')} + + {updateDisabled && ( + + {disabledReason} + + )} + ) diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/ViewUpdateModal.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/ViewUpdateModal.tsx index 7b2207f0bb2..1010313bb1d 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/ViewUpdateModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/ViewUpdateModal.tsx @@ -27,6 +27,7 @@ export function ViewUpdateModal( props: ViewUpdateModalProps ): JSX.Element | null { const { robotName, robot, closeModal } = props + const [showAppUpdateModal, setShowAppUpdateModal] = React.useState(true) const updateInfo = useSelector((state: State) => getRobotUpdateInfo(state, robotName) @@ -38,7 +39,9 @@ export function ViewUpdateModal( getRobotUpdateAvailable(state, robot) ) const robotSystemType = getRobotSystemType(robot) - const availableAppUpdateVersion = useSelector(getAvailableShellUpdate) + const availableAppUpdateVersion = Boolean( + useSelector(getAvailableShellUpdate) + ) const [ showMigrationWarning, @@ -51,12 +54,12 @@ export function ViewUpdateModal( } let releaseNotes = '' - if (updateInfo?.releaseNotes) releaseNotes = updateInfo.releaseNotes + if (updateInfo?.releaseNotes != null) releaseNotes = updateInfo.releaseNotes - if (availableAppUpdateVersion) + if (availableAppUpdateVersion && showAppUpdateModal) return ( - + setShowAppUpdateModal(false)} /> ) diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/RobotUpdateProgressModal.test.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/RobotUpdateProgressModal.test.tsx index 45ae255f3c5..55793ef48dc 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/RobotUpdateProgressModal.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/RobotUpdateProgressModal.test.tsx @@ -8,7 +8,10 @@ import { TIME_BEFORE_ALLOWING_EXIT_MS, } from '../RobotUpdateProgressModal' import { useRobotUpdateInfo } from '../useRobotUpdateInfo' -import { getRobotSessionIsManualFile } from '../../../../../redux/robot-update' +import { + getRobotSessionIsManualFile, + getRobotUpdateDownloadError, +} from '../../../../../redux/robot-update' import { useDispatchStartRobotUpdate } from '../../../../../redux/robot-update/hooks' import type { SetStatusBarCreateCommand } from '@opentrons/shared-data' @@ -31,6 +34,9 @@ const mockGetRobotSessionIsManualFile = getRobotSessionIsManualFile as jest.Mock const mockUseDispatchStartRobotUpdate = useDispatchStartRobotUpdate as jest.MockedFunction< typeof useDispatchStartRobotUpdate > +const mockGetRobotUpdateDownloadError = getRobotUpdateDownloadError as jest.MockedFunction< + typeof getRobotUpdateDownloadError +> const render = ( props: React.ComponentProps @@ -71,12 +77,20 @@ describe('DownloadUpdateModal', () => { }) mockGetRobotSessionIsManualFile.mockReturnValue(false) mockUseDispatchStartRobotUpdate.mockReturnValue(jest.fn) + mockGetRobotUpdateDownloadError.mockReturnValue(null) }) afterEach(() => { jest.resetAllMocks() }) + it('renders robot update download errors', () => { + mockGetRobotUpdateDownloadError.mockReturnValue('test download error') + + const [{ getByText }] = render(props) + getByText('test download error') + }) + it('renders the robot name as a part of the header', () => { const [{ getByText }] = render(props) diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateRobotModal.test.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateRobotModal.test.tsx index d36aab3d057..7af9f5169f3 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateRobotModal.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/UpdateRobotModal.test.tsx @@ -6,9 +6,12 @@ import { fireEvent } from '@testing-library/react' import { renderWithProviders } from '@opentrons/components' import { i18n } from '../../../../../i18n' -import { getRobotUpdateDisplayInfo } from '../../../../../redux/robot-update' +import { + getRobotUpdateDisplayInfo, + getRobotUpdateVersion, +} from '../../../../../redux/robot-update' import { getDiscoverableRobotByName } from '../../../../../redux/discovery' -import { UpdateRobotModal } from '../UpdateRobotModal' +import { UpdateRobotModal, RELEASE_NOTES_URL_BASE } from '../UpdateRobotModal' import type { Store } from 'redux' import type { State } from '../../../../../redux/types' @@ -25,6 +28,9 @@ const mockGetRobotUpdateDisplayInfo = getRobotUpdateDisplayInfo as jest.MockedFu const mockGetDiscoverableRobotByName = getDiscoverableRobotByName as jest.MockedFunction< typeof getDiscoverableRobotByName > +const mockGetRobotUpdateVersion = getRobotUpdateVersion as jest.MockedFunction< + typeof getRobotUpdateVersion +> const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -51,6 +57,7 @@ describe('UpdateRobotModal', () => { updateFromFileDisabledReason: 'test', }) when(mockGetDiscoverableRobotByName).mockReturnValue(null) + when(mockGetRobotUpdateVersion).mockReturnValue('7.0.0') }) afterEach(() => { @@ -93,6 +100,13 @@ describe('UpdateRobotModal', () => { expect(props.closeModal).toHaveBeenCalled() }) + it('renders a release notes link pointing to the Github releases page', () => { + const [{ getByText }] = render(props) + + const link = getByText('Release notes') + expect(link).toHaveAttribute('href', RELEASE_NOTES_URL_BASE + '7.0.0') + }) + it('renders proper text when reinstalling', () => { props = { ...props, diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/useRobotUpdateInfo.test.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/useRobotUpdateInfo.test.tsx index 8b38bf9cde3..f1fd45669fc 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/useRobotUpdateInfo.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/__tests__/useRobotUpdateInfo.test.tsx @@ -1,14 +1,34 @@ +import * as React from 'react' import { renderHook } from '@testing-library/react-hooks' +import { createStore } from 'redux' +import { I18nextProvider } from 'react-i18next' +import { Provider } from 'react-redux' + +import { i18n } from '../../../../../i18n' import { useRobotUpdateInfo } from '../useRobotUpdateInfo' +import { getRobotUpdateDownloadProgress } from '../../../../../redux/robot-update' + +import type { Store } from 'redux' +import { State } from '../../../../../redux/types' import type { RobotUpdateSession, UpdateSessionStep, UpdateSessionStage, } from '../../../../../redux/robot-update/types' +jest.mock('../../../../../redux/robot-update') + +const mockGetRobotUpdateDownloadProgress = getRobotUpdateDownloadProgress as jest.MockedFunction< + typeof getRobotUpdateDownloadProgress +> + describe('useRobotUpdateInfo', () => { + let store: Store + let wrapper: React.FunctionComponent<{}> + + const MOCK_ROBOT_NAME = 'testRobot' const mockRobotUpdateSession: RobotUpdateSession | null = { - robotName: 'testRobot', + robotName: MOCK_ROBOT_NAME, fileInfo: { isManualFile: true, systemFile: 'testFile', version: '1.0.0' }, token: null, pathPrefix: null, @@ -18,36 +38,79 @@ describe('useRobotUpdateInfo', () => { error: null, } - it('should return initial values when session is null', () => { - const { result } = renderHook(() => useRobotUpdateInfo(null)) + beforeEach(() => { + jest.useFakeTimers() + store = createStore(jest.fn(), {}) + store.dispatch = jest.fn() + wrapper = ({ children }) => ( + + {children} + + ) + mockGetRobotUpdateDownloadProgress.mockReturnValue(50) + }) - expect(result.current.updateStep).toBe('initial') + it('should return null when session is null', () => { + const { result } = renderHook( + () => useRobotUpdateInfo(MOCK_ROBOT_NAME, null), + { wrapper } + ) + + expect(result.current.updateStep).toBe(null) expect(result.current.progressPercent).toBe(0) }) it('should return initial values when there is no session step and stage', () => { - const { result } = renderHook(session => useRobotUpdateInfo(session), { - initialProps: { - ...mockRobotUpdateSession, - step: null, - stage: null, - }, - }) + const { result } = renderHook( + session => useRobotUpdateInfo(MOCK_ROBOT_NAME, session), + { + initialProps: { + ...mockRobotUpdateSession, + step: null, + stage: null, + } as any, + wrapper, + } + ) expect(result.current.updateStep).toBe('initial') expect(result.current.progressPercent).toBe(0) }) + it('should return download updateStep and appropriate percentages when the update is downloading', () => { + const { result, rerender } = renderHook( + session => useRobotUpdateInfo(MOCK_ROBOT_NAME, session), + { + initialProps: { + ...mockRobotUpdateSession, + step: 'downloadFile', + } as any, + wrapper, + } + ) + + expect(result.current.updateStep).toBe('download') + expect(Math.round(result.current.progressPercent)).toBe(17) + + rerender({ + ...mockRobotUpdateSession, + }) + + expect(result.current.updateStep).toBe('install') + expect(result.current.progressPercent).toBe(50) + }) + it('should update updateStep and progressPercent when session is provided', () => { const { result, rerender } = renderHook( - session => useRobotUpdateInfo(session), + session => useRobotUpdateInfo(MOCK_ROBOT_NAME, session), { - initialProps: mockRobotUpdateSession, + initialProps: mockRobotUpdateSession as any, + wrapper, } ) expect(result.current.updateStep).toBe('install') - expect(Math.round(result.current.progressPercent)).toBe(75) + expect(Math.round(result.current.progressPercent)).toBe(25) rerender({ ...mockRobotUpdateSession, @@ -63,14 +126,15 @@ describe('useRobotUpdateInfo', () => { it('should return correct updateStep and progressPercent values when there is an error', () => { const { result, rerender } = renderHook( - session => useRobotUpdateInfo(session), + session => useRobotUpdateInfo(MOCK_ROBOT_NAME, session), { - initialProps: mockRobotUpdateSession, + initialProps: mockRobotUpdateSession as any, + wrapper, } ) expect(result.current.updateStep).toBe('install') - expect(Math.round(result.current.progressPercent)).toBe(75) + expect(Math.round(result.current.progressPercent)).toBe(25) rerender({ ...mockRobotUpdateSession, @@ -78,34 +142,42 @@ describe('useRobotUpdateInfo', () => { }) expect(result.current.updateStep).toBe('error') - expect(Math.round(result.current.progressPercent)).toBe(75) + expect(Math.round(result.current.progressPercent)).toBe(25) }) it('should calculate correct progressPercent when the update is not manual', () => { - const { result } = renderHook(session => useRobotUpdateInfo(session), { - initialProps: { - ...mockRobotUpdateSession, - fileInfo: { - systemFile: 'downloadPath', - version: '1.0.0', - isManualFile: false, - }, - }, - }) + const { result } = renderHook( + session => useRobotUpdateInfo(MOCK_ROBOT_NAME, session), + { + initialProps: { + ...mockRobotUpdateSession, + fileInfo: { + systemFile: 'downloadPath', + version: '1.0.0', + isManualFile: false, + }, + } as any, + wrapper, + } + ) expect(result.current.updateStep).toBe('install') - expect(Math.round(result.current.progressPercent)).toBe(75) + expect(Math.round(result.current.progressPercent)).toBe(25) }) it('should ignore progressPercent reported by a step marked as ignored', () => { - const { result } = renderHook(session => useRobotUpdateInfo(session), { - initialProps: { - ...mockRobotUpdateSession, - step: 'processFile' as UpdateSessionStep, - stage: 'awaiting-file' as UpdateSessionStage, - progress: 100, - }, - }) + const { result } = renderHook( + session => useRobotUpdateInfo(MOCK_ROBOT_NAME, session), + { + initialProps: { + ...mockRobotUpdateSession, + step: 'processFile' as UpdateSessionStep, + stage: 'awaiting-file' as UpdateSessionStage, + progress: 100, + } as any, + wrapper, + } + ) expect(result.current.updateStep).toBe('install') expect(Math.round(result.current.progressPercent)).toBe(0) diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/useRobotUpdateInfo.ts b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/useRobotUpdateInfo.ts index 3e1b6e60a56..75c35746c9d 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/useRobotUpdateInfo.ts +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/useRobotUpdateInfo.ts @@ -1,16 +1,20 @@ import * as React from 'react' +import { useSelector } from 'react-redux' + +import { getRobotUpdateDownloadProgress } from '../../../../redux/robot-update' + import type { RobotUpdateSession } from '../../../../redux/robot-update/types' +import type { State } from '../../../../redux/types' export function useRobotUpdateInfo( + robotName: string, session: RobotUpdateSession | null -): { updateStep: UpdateStep; progressPercent: number } { - const progressPercent = useFindProgressPercentFrom(session) +): { updateStep: UpdateStep | null; progressPercent: number } { + const progressPercent = useFindProgressPercentFrom(robotName, session) - const shellReportedUpdateStep = React.useMemo( - () => getShellReportedUpdateStep(session), - [session] - ) - const updateStep = useTransitionUpdateStepFrom(shellReportedUpdateStep) + const updateStep = React.useMemo(() => determineUpdateStepFrom(session), [ + session, + ]) return { updateStep, @@ -19,22 +23,35 @@ export function useRobotUpdateInfo( } function useFindProgressPercentFrom( + robotName: string, session: RobotUpdateSession | null ): number { const [progressPercent, setProgressPercent] = React.useState(0) + const hasSeenDownloadFileStep = React.useRef(false) const prevSeenUpdateStep = React.useRef(null) const prevSeenStepProgress = React.useRef(0) const currentStepWithProgress = React.useRef(-1) - if (session == null) return progressPercent + const downloadProgress = useSelector((state: State) => + getRobotUpdateDownloadProgress(state, robotName) + ) + + if (session == null) { + if (progressPercent !== 0) { + setProgressPercent(0) + prevSeenStepProgress.current = 0 + hasSeenDownloadFileStep.current = false + } + return progressPercent + } - const { + let { step: sessionStep, stage: sessionStage, progress: stepProgress, } = session - if (sessionStep === 'getToken') { + if (sessionStep == null && sessionStage == null) { if (progressPercent !== 0) { setProgressPercent(0) prevSeenStepProgress.current = 0 @@ -46,27 +63,30 @@ function useFindProgressPercentFrom( prevSeenStepProgress.current = 100 } return progressPercent - } else if ( - sessionStage === 'error' || - sessionStage === null || - stepProgress == null || - sessionStep == null - ) { + } else if (sessionStage === 'error') { return progressPercent } const stepAndStage = `${sessionStep}-${sessionStage}` - // Ignored because 0-100 is too fast to be worth recording. + // Ignored because 0-100 is too fast to be worth recording or currently unsupported. const IGNORED_STEPS_AND_STAGES = [ 'processFile-awaiting-file', 'uploadFile-awaiting-file', ] + + if (sessionStep === 'downloadFile') { + hasSeenDownloadFileStep.current = true + stepProgress = downloadProgress + } + + stepProgress = stepProgress ?? 0 + // Each stepAndStage is an equal fraction of the total steps. - const TOTAL_STEPS_WITH_PROGRESS = 2 + const TOTAL_STEPS_WITH_PROGRESS = hasSeenDownloadFileStep.current ? 3 : 2 const isNewStateWithProgress = prevSeenUpdateStep.current !== stepAndStage && - stepProgress > 0 && // Accomodate for shell progress oddities. + stepProgress > 0 && // Accommodate for shell progress oddities. stepProgress < 100 // Proceed to next fraction of progress bar. @@ -79,7 +99,7 @@ function useFindProgressPercentFrom( (100 * currentStepWithProgress.current) / TOTAL_STEPS_WITH_PROGRESS prevSeenStepProgress.current = 0 prevSeenUpdateStep.current = stepAndStage - setProgressPercent(completedStepsWithProgress + stepProgress) + setProgressPercent(completedStepsWithProgress) } // Proceed with current fraction of progress bar. else if ( @@ -87,12 +107,13 @@ function useFindProgressPercentFrom( !IGNORED_STEPS_AND_STAGES.includes(stepAndStage) ) { const currentStepProgress = - progressPercent + (stepProgress - prevSeenStepProgress.current) / TOTAL_STEPS_WITH_PROGRESS + const nonBacktrackedProgressPercent = Math.max( progressPercent, - currentStepProgress + currentStepProgress + progressPercent ) + prevSeenStepProgress.current = stepProgress setProgressPercent(nonBacktrackedProgressPercent) } @@ -108,18 +129,19 @@ export type UpdateStep = | 'finished' | 'error' -function getShellReportedUpdateStep( +function determineUpdateStepFrom( session: RobotUpdateSession | null ): UpdateStep | null { if (session == null) return null const { step: sessionStep, stage: sessionStage, error } = session - // TODO(jh, 09-14-2023: add download logic to app-shell/redux/progress bar. let reportedUpdateStep: UpdateStep if (error != null) { reportedUpdateStep = 'error' } else if (sessionStep == null && sessionStage == null) { reportedUpdateStep = 'initial' + } else if (sessionStep === 'downloadFile') { + reportedUpdateStep = 'download' } else if (sessionStep === 'finished') { reportedUpdateStep = 'finished' } else if ( @@ -134,44 +156,3 @@ function getShellReportedUpdateStep( return reportedUpdateStep } - -// Shell steps have the potential to backtrack, so use guarded transitions. -function useTransitionUpdateStepFrom( - reportedUpdateStep: UpdateStep | null -): UpdateStep { - const [updateStep, setUpdateStep] = React.useState('initial') - const prevUpdateStep = React.useRef(null) - - switch (reportedUpdateStep) { - case 'initial': - if (updateStep !== 'initial') { - setUpdateStep('initial') - prevUpdateStep.current = null - } - break - case 'error': - if (updateStep !== 'error') { - setUpdateStep('error') - } - break - case 'install': - if (updateStep === 'initial' && prevUpdateStep.current == null) { - setUpdateStep('install') - prevUpdateStep.current = 'initial' - } - break - case 'restart': - if (updateStep === 'install' && prevUpdateStep.current === 'initial') { - setUpdateStep('restart') - prevUpdateStep.current = 'install' - } - break - case 'finished': - if (updateStep === 'restart' && prevUpdateStep.current === 'install') { - setUpdateStep('finished') - prevUpdateStep.current = 'restart' - } - break - } - return updateStep -} diff --git a/app/src/organisms/Devices/RobotSettings/__tests__/SelectNetwork.test.tsx b/app/src/organisms/Devices/RobotSettings/__tests__/SelectNetwork.test.tsx index f236c74046b..9223b07ddd5 100644 --- a/app/src/organisms/Devices/RobotSettings/__tests__/SelectNetwork.test.tsx +++ b/app/src/organisms/Devices/RobotSettings/__tests__/SelectNetwork.test.tsx @@ -17,9 +17,10 @@ import { ConnectModal } from '../ConnectNetwork/ConnectModal' import { ResultModal } from '../ConnectNetwork/ResultModal' import { SelectNetwork } from '../SelectNetwork' +import { RequestState } from '../../../../redux/robot-api/types' +import { PENDING, SUCCESS, FAILURE } from '../../../../redux/robot-api' import type { ReactWrapper } from 'enzyme' -import { RequestState } from '../../../../redux/robot-api/types' jest.mock('../../../../resources/networking/hooks') jest.mock('../../../../redux/networking/selectors') @@ -260,7 +261,7 @@ describe('', () => { expect(resultModal.props()).toEqual({ type: Constants.CONNECT, ssid: mockConfigure.ssid, - isPending: true, + requestStatus: PENDING, error: null, onClose: expect.any(Function), }) @@ -278,7 +279,7 @@ describe('', () => { expect(resultModal.props()).toEqual({ type: Constants.CONNECT, ssid: mockConfigure.ssid, - isPending: false, + requestStatus: SUCCESS, error: null, onClose: expect.any(Function), }) @@ -307,7 +308,7 @@ describe('', () => { expect(resultModal.props()).toEqual({ type: Constants.CONNECT, ssid: mockConfigure.ssid, - isPending: false, + requestStatus: FAILURE, error: { message: 'oh no!' }, onClose: expect.any(Function), }) diff --git a/app/src/organisms/Devices/__tests__/RecentProtocolRuns.test.tsx b/app/src/organisms/Devices/__tests__/RecentProtocolRuns.test.tsx index 570da04652d..2cfb460f4a7 100644 --- a/app/src/organisms/Devices/__tests__/RecentProtocolRuns.test.tsx +++ b/app/src/organisms/Devices/__tests__/RecentProtocolRuns.test.tsx @@ -8,6 +8,7 @@ import { RecentProtocolRuns } from '../RecentProtocolRuns' import { HistoricalProtocolRun } from '../HistoricalProtocolRun' import type { Runs } from '@opentrons/api-client' +import type { AxiosError } from 'axios' jest.mock('@opentrons/react-api-client') jest.mock('../hooks') @@ -57,7 +58,7 @@ describe('RecentProtocolRuns', () => { mockUseIsRobotViewable.mockReturnValue(true) mockUseAllRunsQuery.mockReturnValue({ data: {}, - } as UseQueryResult) + } as UseQueryResult) const [{ getByText }] = render() getByText('No protocol runs yet!') @@ -76,7 +77,7 @@ describe('RecentProtocolRuns', () => { }, ], }, - } as UseQueryResult) + } as UseQueryResult) const [{ getByText }] = render() getByText('Recent Protocol Runs') getByText('Run') diff --git a/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts b/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts index 559ea98da67..fd6a466c5e8 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts +++ b/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts @@ -14,6 +14,7 @@ import { useIsRobotBusy } from '../useIsRobotBusy' import { useIsFlex } from '../useIsFlex' import type { Sessions, Runs } from '@opentrons/api-client' +import type { AxiosError } from 'axios' jest.mock('@opentrons/react-api-client') jest.mock('../../../ProtocolUpload/hooks') @@ -49,7 +50,7 @@ describe('useIsRobotBusy', () => { current: {}, }, }, - } as UseQueryResult) + } as UseQueryResult) mockUseEstopQuery.mockReturnValue({ data: mockEstopStatus } as any) mockUseIsFlex.mockReturnValue(false) }) diff --git a/app/src/organisms/Devices/hooks/__tests__/useLabwareRenderInfoForRunById.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useLabwareRenderInfoForRunById.test.tsx index 2f596a43c23..fa6e2ed8996 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useLabwareRenderInfoForRunById.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useLabwareRenderInfoForRunById.test.tsx @@ -1,7 +1,7 @@ import { renderHook } from '@testing-library/react-hooks' import { when, resetAllWhenMocks } from 'jest-when' -import standardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot2_standard.json' +import standardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot2_standard.json' import _heaterShakerCommandsWithResultsKey from '@opentrons/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' diff --git a/app/src/organisms/Devices/hooks/__tests__/useModuleCalibrationStatus.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useModuleCalibrationStatus.test.tsx index d9915af41c6..56e0feec2e5 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useModuleCalibrationStatus.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useModuleCalibrationStatus.test.tsx @@ -86,7 +86,7 @@ describe('useModuleCalibrationStatus hook', () => { it('should return calibration complete if OT-2', () => { when(mockUseIsFlex).calledWith('otie').mockReturnValue(false) when(mockUseModuleRenderInfoForProtocolById) - .calledWith('otie', '1') + .calledWith('1') .mockReturnValue({}) const { result } = renderHook( @@ -100,7 +100,7 @@ describe('useModuleCalibrationStatus hook', () => { it('should return calibration complete if no modules needed', () => { when(mockUseIsFlex).calledWith('otie').mockReturnValue(true) when(mockUseModuleRenderInfoForProtocolById) - .calledWith('otie', '1') + .calledWith('1') .mockReturnValue({}) const { result } = renderHook( @@ -114,7 +114,7 @@ describe('useModuleCalibrationStatus hook', () => { it('should return calibration complete if offset date exists', () => { when(mockUseIsFlex).calledWith('otie').mockReturnValue(true) when(mockUseModuleRenderInfoForProtocolById) - .calledWith('otie', '1') + .calledWith('1') .mockReturnValue({ magneticModuleId: { attachedModuleMatch: { @@ -136,7 +136,7 @@ describe('useModuleCalibrationStatus hook', () => { it('should return calibration needed if offset date does not exist', () => { when(mockUseIsFlex).calledWith('otie').mockReturnValue(true) when(mockUseModuleRenderInfoForProtocolById) - .calledWith('otie', '1') + .calledWith('1') .mockReturnValue({ magneticModuleId: { attachedModuleMatch: { diff --git a/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx index 4946e81aa7d..575e349a5a8 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useModuleRenderInfoForProtocolById.test.tsx @@ -2,13 +2,7 @@ import { renderHook } from '@testing-library/react-hooks' import { when, resetAllWhenMocks } from 'jest-when' import { UseQueryResult } from 'react-query' -import { - getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, - FLEX_ROBOT_TYPE, - STAGING_AREA_LOAD_NAME, -} from '@opentrons/shared-data' -import standardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json' +import { STAGING_AREA_RIGHT_SLOT_FIXTURE } from '@opentrons/shared-data' import _heaterShakerCommandsWithResultsKey from '@opentrons/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useDeckConfigurationQuery } from '@opentrons/react-api-client/src/deck_configuration' @@ -27,16 +21,14 @@ import { } from '..' import type { + CutoutConfig, DeckConfiguration, - DeckDefinition, - Fixture, ModuleModel, ModuleType, ProtocolAnalysisOutput, } from '@opentrons/shared-data' jest.mock('@opentrons/react-api-client/src/deck_configuration') -jest.mock('@opentrons/shared-data/js/helpers') jest.mock('../../ProtocolRun/utils/getProtocolModulesInfo') jest.mock('../useAttachedModules') jest.mock('../useProtocolDetailsForRun') @@ -49,12 +41,6 @@ const mockGetProtocolModulesInfo = getProtocolModulesInfo as jest.MockedFunction const mockUseAttachedModules = useAttachedModules as jest.MockedFunction< typeof useAttachedModules > -const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction< - typeof getDeckDefFromRobotType -> -const mockGetRobotTypeFromLoadedLabware = getRobotTypeFromLoadedLabware as jest.MockedFunction< - typeof getRobotTypeFromLoadedLabware -> const mockUseStoredProtocolAnalysis = useStoredProtocolAnalysis as jest.MockedFunction< typeof useStoredProtocolAnalysis > @@ -68,7 +54,15 @@ const heaterShakerCommandsWithResultsKey = (_heaterShakerCommandsWithResultsKey const PROTOCOL_DETAILS = { displayName: 'fake protocol', - protocolData: heaterShakerCommandsWithResultsKey, + protocolData: { + ...heaterShakerCommandsWithResultsKey, + labware: [ + { + displayName: 'Trash', + definitionId: 'opentrons/opentrons_1_trash_3200ml_fixed/1', + }, + ], + }, protocolKey: 'fakeProtocolKey', } @@ -132,16 +126,15 @@ const TEMPERATURE_MODULE_INFO = { slotName: 'D1', } -const mockFixture = { - fixtureId: 'mockId', - fixtureLocation: 'D1', - loadName: STAGING_AREA_LOAD_NAME, -} as Fixture +const mockCutoutConfig: CutoutConfig = { + cutoutId: 'cutoutD1', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, +} describe('useModuleRenderInfoForProtocolById hook', () => { beforeEach(() => { when(mockUseDeckConfigurationQuery).mockReturnValue({ - data: [mockFixture], + data: [mockCutoutConfig], } as UseQueryResult) when(mockUseAttachedModules) .calledWith() @@ -150,22 +143,16 @@ describe('useModuleRenderInfoForProtocolById hook', () => { mockTemperatureModuleGen2, mockThermocycler, ]) - when(mockGetDeckDefFromRobotType) - .calledWith(FLEX_ROBOT_TYPE) - .mockReturnValue((standardDeckDef as unknown) as DeckDefinition) - when(mockGetRobotTypeFromLoadedLabware).mockReturnValue(FLEX_ROBOT_TYPE) when(mockUseStoredProtocolAnalysis) .calledWith('1') .mockReturnValue((PROTOCOL_DETAILS as unknown) as ProtocolAnalysisOutput) when(mockUseMostRecentCompletedAnalysis) .calledWith('1') .mockReturnValue(PROTOCOL_DETAILS.protocolData as any) - when(mockGetProtocolModulesInfo) - .calledWith( - heaterShakerCommandsWithResultsKey, - (standardDeckDef as unknown) as DeckDefinition - ) - .mockReturnValue([TEMPERATURE_MODULE_INFO, MAGNETIC_MODULE_INFO]) + mockGetProtocolModulesInfo.mockReturnValue([ + TEMPERATURE_MODULE_INFO, + MAGNETIC_MODULE_INFO, + ]) }) afterEach(() => { @@ -176,23 +163,19 @@ describe('useModuleRenderInfoForProtocolById hook', () => { .calledWith('1') .mockReturnValue(null) when(mockUseStoredProtocolAnalysis).calledWith('1').mockReturnValue(null) - const { result } = renderHook(() => - useModuleRenderInfoForProtocolById('otie', '1') - ) + const { result } = renderHook(() => useModuleRenderInfoForProtocolById('1')) expect(result.current).toStrictEqual({}) }) it('should return module render info', () => { - const { result } = renderHook(() => - useModuleRenderInfoForProtocolById('otie', '1') - ) + const { result } = renderHook(() => useModuleRenderInfoForProtocolById('1')) expect(result.current).toStrictEqual({ magneticModuleId: { - conflictedFixture: mockFixture, + conflictedFixture: mockCutoutConfig, attachedModuleMatch: mockMagneticModuleGen2, ...MAGNETIC_MODULE_INFO, }, temperatureModuleId: { - conflictedFixture: mockFixture, + conflictedFixture: mockCutoutConfig, attachedModuleMatch: mockTemperatureModuleGen2, ...TEMPERATURE_MODULE_INFO, }, diff --git a/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx index 2c94afeedf1..84329276960 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx @@ -13,7 +13,10 @@ import { useProtocolDetailsForRun } from '..' import { RUN_ID_2 } from '../../../../organisms/RunTimeControl/__fixtures__' import type { Protocol, Run } from '@opentrons/api-client' -import { CompletedProtocolAnalysis } from '@opentrons/shared-data' +import { + CompletedProtocolAnalysis, + OT2_ROBOT_TYPE, +} from '@opentrons/shared-data' jest.mock('@opentrons/react-api-client') @@ -39,6 +42,7 @@ const PROTOCOL_RESPONSE = { metadata: { protocolName: 'fake protocol' }, analysisSummaries: [{ id: PROTOCOL_ANALYSIS.id, status: 'completed' }], key: 'fakeProtocolKey', + robotType: OT2_ROBOT_TYPE, }, } as Protocol @@ -68,7 +72,7 @@ describe('useProtocolDetailsForRun hook', () => { protocolData: null, protocolKey: null, isProtocolAnalyzing: false, - robotType: 'OT-2 Standard', + robotType: 'OT-3 Standard', }) }) diff --git a/app/src/organisms/Devices/hooks/__tests__/useUnmatchedModulesForProtocol.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useUnmatchedModulesForProtocol.test.tsx index e524aa53d5a..971df5dc939 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useUnmatchedModulesForProtocol.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useUnmatchedModulesForProtocol.test.tsx @@ -51,7 +51,7 @@ describe('useModuleMatchResults', () => { .calledWith(mockConnectedRobot.name) .mockReturnValue(mockConnectedRobot) when(mockUseModuleRenderInfoForProtocolById) - .calledWith(mockConnectedRobot.name, '1') + .calledWith('1') .mockReturnValue({}) when(mockUseAttachedModules) @@ -67,7 +67,7 @@ describe('useModuleMatchResults', () => { when(mockUseAttachedModules).calledWith().mockReturnValue([]) const moduleId = 'fakeMagBlockId' when(mockUseModuleRenderInfoForProtocolById) - .calledWith(mockConnectedRobot.name, '1') + .calledWith('1') .mockReturnValue({ [moduleId]: { moduleId: moduleId, @@ -94,7 +94,7 @@ describe('useModuleMatchResults', () => { it('should return 1 missing moduleId if requested model not attached', () => { const moduleId = 'fakeMagModuleId' when(mockUseModuleRenderInfoForProtocolById) - .calledWith(mockConnectedRobot.name, '1') + .calledWith('1') .mockReturnValue({ [moduleId]: { moduleId: moduleId, @@ -121,7 +121,7 @@ describe('useModuleMatchResults', () => { it('should return no missing moduleId if compatible model is attached', () => { const moduleId = 'someTempModule' when(mockUseModuleRenderInfoForProtocolById) - .calledWith(mockConnectedRobot.name, '1') + .calledWith('1') .mockReturnValue({ [moduleId]: { moduleId: moduleId, @@ -147,7 +147,7 @@ describe('useModuleMatchResults', () => { it('should return one missing moduleId if nocompatible model is attached', () => { const moduleId = 'someTempModule' when(mockUseModuleRenderInfoForProtocolById) - .calledWith(mockConnectedRobot.name, '1') + .calledWith('1') .mockReturnValue({ [moduleId]: { moduleId: moduleId, diff --git a/app/src/organisms/Devices/hooks/useModuleCalibrationStatus.ts b/app/src/organisms/Devices/hooks/useModuleCalibrationStatus.ts index a484e0cd7a0..e8bddaaeadb 100644 --- a/app/src/organisms/Devices/hooks/useModuleCalibrationStatus.ts +++ b/app/src/organisms/Devices/hooks/useModuleCalibrationStatus.ts @@ -11,7 +11,7 @@ export function useModuleCalibrationStatus( const isFlex = useIsFlex(robotName) // TODO: can probably use getProtocolModulesInfo but in a rush to get out 7.0.1 const moduleRenderInfoForProtocolById = omitBy( - useModuleRenderInfoForProtocolById(robotName, runId), + useModuleRenderInfoForProtocolById(runId), moduleRenderInfo => moduleRenderInfo.moduleDef.moduleType === MAGNETIC_BLOCK_TYPE ) diff --git a/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts b/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts index 3c5d9404f74..1c9570dfcde 100644 --- a/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts +++ b/app/src/organisms/Devices/hooks/useModuleRenderInfoForProtocolById.ts @@ -1,23 +1,24 @@ import { checkModuleCompatibility, - Fixture, + FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, - STANDARD_SLOT_LOAD_NAME, + SINGLE_SLOT_FIXTURES, } from '@opentrons/shared-data' import { useDeckConfigurationQuery } from '@opentrons/react-api-client/src/deck_configuration' +import { getCutoutIdForSlotName } from '../../../resources/deck_configuration/utils' import { getProtocolModulesInfo } from '../ProtocolRun/utils/getProtocolModulesInfo' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useAttachedModules } from './useAttachedModules' import { useStoredProtocolAnalysis } from './useStoredProtocolAnalysis' +import type { CutoutConfig } from '@opentrons/shared-data' import type { AttachedModule } from '../../../redux/modules/types' import type { ProtocolModuleInfo } from '../ProtocolRun/utils/getProtocolModulesInfo' export interface ModuleRenderInfoForProtocol extends ProtocolModuleInfo { attachedModuleMatch: AttachedModule | null - conflictedFixture?: Fixture + conflictedFixture?: CutoutConfig } export interface ModuleRenderInfoById { @@ -25,20 +26,20 @@ export interface ModuleRenderInfoById { } export function useModuleRenderInfoForProtocolById( - robotName: string, runId: string ): ModuleRenderInfoById { const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId) const { data: deckConfig } = useDeckConfigurationQuery() const storedProtocolAnalysis = useStoredProtocolAnalysis(runId) - const protocolData = robotProtocolAnalysis ?? storedProtocolAnalysis - const robotType = getRobotTypeFromLoadedLabware(protocolData?.labware ?? []) + const protocolAnalysis = robotProtocolAnalysis ?? storedProtocolAnalysis const attachedModules = useAttachedModules() - if (protocolData == null) return {} + if (protocolAnalysis == null) return {} - const deckDef = getDeckDefFromRobotType(robotType) + const deckDef = getDeckDefFromRobotType( + protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE + ) - const protocolModulesInfo = getProtocolModulesInfo(protocolData, deckDef) + const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef) const protocolModulesInfoInLoadOrder = protocolModulesInfo.sort( (modA, modB) => modA.protocolLoadOrder - modB.protocolLoadOrder @@ -54,26 +55,31 @@ export function useModuleRenderInfoForProtocolById( protocolMod.moduleDef.model ) && !matchedAmod.find(m => m === attachedMod) ) ?? null + + const cutoutIdForSlotName = getCutoutIdForSlotName( + protocolMod.slotName, + deckDef + ) + + const conflictedFixture = deckConfig?.find( + fixture => + fixture.cutoutId === cutoutIdForSlotName && + fixture.cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) + ) + if (compatibleAttachedModule !== null) { matchedAmod = [...matchedAmod, compatibleAttachedModule] return { ...protocolMod, attachedModuleMatch: compatibleAttachedModule, - conflictedFixture: deckConfig?.find( - fixture => - fixture.fixtureLocation === protocolMod.slotName && - fixture.loadName !== STANDARD_SLOT_LOAD_NAME - ), + conflictedFixture, } } return { ...protocolMod, attachedModuleMatch: null, - conflictedFixture: deckConfig?.find( - fixture => - fixture.fixtureLocation === protocolMod.slotName && - fixture.loadName !== STANDARD_SLOT_LOAD_NAME - ), + conflictedFixture, } } ) diff --git a/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts b/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts index ec61b88967f..c8c7b3c9b36 100644 --- a/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts +++ b/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts @@ -1,6 +1,6 @@ import * as React from 'react' import last from 'lodash/last' -import { getRobotTypeFromLoadedLabware } from '@opentrons/shared-data' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { useProtocolQuery, useRunQuery, @@ -61,10 +61,10 @@ export function useProtocolDetailsForRun( protocolData: mostRecentAnalysis ?? null, protocolKey: protocolRecord?.data.key ?? null, isProtocolAnalyzing: protocolRecord != null && mostRecentAnalysis == null, - // this should be deleted as soon as analysis tells us intended robot type robotType: - mostRecentAnalysis?.status === 'completed' - ? getRobotTypeFromLoadedLabware(mostRecentAnalysis.labware) - : 'OT-2 Standard', + protocolRecord?.data.robotType ?? + (mostRecentAnalysis?.status === 'completed' + ? mostRecentAnalysis?.robotType ?? FLEX_ROBOT_TYPE + : FLEX_ROBOT_TYPE), } } diff --git a/app/src/organisms/Devices/hooks/useRobotAnalyticsData.ts b/app/src/organisms/Devices/hooks/useRobotAnalyticsData.ts index c1289e0da83..6ca8c95fbca 100644 --- a/app/src/organisms/Devices/hooks/useRobotAnalyticsData.ts +++ b/app/src/organisms/Devices/hooks/useRobotAnalyticsData.ts @@ -4,7 +4,6 @@ import { useSelector, useDispatch } from 'react-redux' import { useRobot } from './' import { getAttachedPipettes } from '../../../redux/pipettes' import { getRobotSettings, fetchSettings } from '../../../redux/robot-settings' -import { FF_PREFIX } from '../../../redux/analytics' import { getRobotApiVersion, getRobotFirmwareVersion, @@ -13,6 +12,8 @@ import { import type { State, Dispatch } from '../../../redux/types' import type { RobotAnalyticsData } from '../../../redux/analytics/types' +const FF_PREFIX = 'robotFF_' + /** * * @param {string} robotName @@ -45,10 +46,10 @@ export function useRobotAnalyticsData( }), // @ts-expect-error RobotAnalyticsData type needs boolean values should it be boolean | string { - robotApiServerVersion: getRobotApiVersion(robot) || '', - robotSmoothieVersion: getRobotFirmwareVersion(robot) || '', - robotLeftPipette: pipettes.left?.model || '', - robotRightPipette: pipettes.right?.model || '', + robotApiServerVersion: getRobotApiVersion(robot) ?? '', + robotSmoothieVersion: getRobotFirmwareVersion(robot) ?? '', + robotLeftPipette: pipettes.left?.model ?? '', + robotRightPipette: pipettes.right?.model ?? '', } ) } diff --git a/app/src/organisms/Devices/hooks/useUnmatchedModulesForProtocol.ts b/app/src/organisms/Devices/hooks/useUnmatchedModulesForProtocol.ts index 5e6d52e292d..9c76e899c45 100644 --- a/app/src/organisms/Devices/hooks/useUnmatchedModulesForProtocol.ts +++ b/app/src/organisms/Devices/hooks/useUnmatchedModulesForProtocol.ts @@ -23,10 +23,7 @@ export function useUnmatchedModulesForProtocol( runId: string ): UnmatchedModuleResults { const robot = useRobot(robotName) - const moduleRenderInfoById = useModuleRenderInfoForProtocolById( - robotName, - runId - ) + const moduleRenderInfoById = useModuleRenderInfoForProtocolById(runId) const attachedModules = useAttachedModules() if (robot === null) { return { missingModuleIds: [], remainingAttachedModules: [] } diff --git a/app/src/organisms/DropTipWizard/ChooseLocation.tsx b/app/src/organisms/DropTipWizard/ChooseLocation.tsx index deaedcc7b0a..72bdf5c1d85 100644 --- a/app/src/organisms/DropTipWizard/ChooseLocation.tsx +++ b/app/src/organisms/DropTipWizard/ChooseLocation.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import { css } from 'styled-components' import { useTranslation } from 'react-i18next' + import { Flex, DIRECTION_COLUMN, @@ -14,13 +15,19 @@ import { SPACING, TYPOGRAPHY, } from '@opentrons/components' +import { + getDeckDefFromRobotType, + getPositionFromSlotId, +} from '@opentrons/shared-data' + +import { SmallButton } from '../../atoms/buttons' import { StyledText } from '../../atoms/text' +import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' // import { NeedHelpLink } from '../CalibrationPanels' import { TwoUpTileLayout } from '../LabwarePositionCheck/TwoUpTileLayout' -import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' -import { RobotType, getDeckDefFromRobotType } from '@opentrons/shared-data' + import type { CommandData } from '@opentrons/api-client' -import { SmallButton } from '../../atoms/buttons' +import type { RobotType } from '@opentrons/shared-data' // TODO: get help link article URL // const NEED_HELP_URL = '' @@ -56,13 +63,19 @@ export const ChooseLocation = ( ) const handleConfirmPosition: React.MouseEventHandler = () => { - const deckLocation = deckDef.locations.orderedSlots.find( + const deckSlot = deckDef.locations.addressableAreas.find( l => l.id === selectedLocation.slotName ) - const slotX = deckLocation?.position[0] - const slotY = deckLocation?.position[1] - const xDimension = deckLocation?.boundingBox.xDimension - const yDimension = deckLocation?.boundingBox.yDimension + + const slotPosition = getPositionFromSlotId( + selectedLocation.slotName, + deckDef + ) + + const slotX = slotPosition?.[0] + const slotY = slotPosition?.[1] + const xDimension = deckSlot?.boundingBox.xDimension + const yDimension = deckSlot?.boundingBox.yDimension if ( slotX != null && slotY != null && diff --git a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx index aab4b2b359d..8d4925ce73b 100644 --- a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx +++ b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx @@ -33,7 +33,6 @@ import { getModuleDisplayName, getModuleType, getOccludedSlotCountForModule, - getRobotTypeFromLoadedLabware, } from '@opentrons/shared-data' import { getRunLabwareRenderInfo, @@ -49,6 +48,7 @@ import { getLoadedModule, } from '../CommandText/utils/accessors' import type { RunData } from '@opentrons/api-client' +import { useDeckConfigurationQuery } from '@opentrons/react-api-client' const LABWARE_DESCRIPTION_STYLE = css` flex-direction: ${DIRECTION_COLUMN}; @@ -103,6 +103,7 @@ export interface MoveLabwareInterventionProps { command: MoveLabwareRunTimeCommand analysis: CompletedProtocolAnalysis | null run: RunData + robotType: RobotType isOnDevice: boolean } @@ -110,14 +111,15 @@ export function MoveLabwareInterventionContent({ command, analysis, run, + robotType, isOnDevice, }: MoveLabwareInterventionProps): JSX.Element | null { const { t } = useTranslation(['protocol_setup', 'protocol_command_text']) const analysisCommands = analysis?.commands ?? [] const labwareDefsByUri = getLoadedLabwareDefinitionsByUri(analysisCommands) - const robotType = getRobotTypeFromLoadedLabware(run.labware) const deckDef = getDeckDefFromRobotType(robotType) + const deckConfig = useDeckConfigurationQuery().data ?? [] const moduleRenderInfo = getRunModuleRenderInfo( run, @@ -196,8 +198,7 @@ export function MoveLabwareInterventionContent({ movedLabwareDef={movedLabwareDef} loadedModules={run.modules} loadedLabware={run.labware} - // TODO(bh, 2023-07-19): read trash slot name from protocol - trashLocation={robotType === 'OT-3 Standard' ? 'A3' : undefined} + deckConfig={deckConfig} backgroundItems={ <> {moduleRenderInfo.map( diff --git a/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx b/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx index 2c937472b64..cd320a21058 100644 --- a/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx +++ b/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx @@ -15,11 +15,16 @@ import { truncatedCommandMessage, } from '../__fixtures__' import { mockTipRackDefinition } from '../../../redux/custom-labware/__fixtures__' +import { useIsFlex } from '../../Devices/hooks' const ROBOT_NAME = 'Otie' const mockOnResumeHandler = jest.fn() +jest.mock('../../Devices/hooks') + +const mockUseIsFlex = useIsFlex as jest.MockedFunction + const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -43,6 +48,7 @@ describe('InterventionModal', () => { ], } as CompletedProtocolAnalysis, } + mockUseIsFlex.mockReturnValue(true) }) it('renders an InterventionModal with the robot name in the header and confirm button', () => { diff --git a/app/src/organisms/InterventionModal/__tests__/utils.test.ts b/app/src/organisms/InterventionModal/__tests__/utils.test.ts index 8aa5b57b6bb..2e27af4bbac 100644 --- a/app/src/organisms/InterventionModal/__tests__/utils.test.ts +++ b/app/src/organisms/InterventionModal/__tests__/utils.test.ts @@ -1,7 +1,7 @@ import deepClone from 'lodash/cloneDeep' import { getSlotHasMatingSurfaceUnitVector } from '@opentrons/shared-data' -import deckDefFixture from '@opentrons/shared-data/deck/fixtures/3/deckExample.json' +import standardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot2_standard.json' import { mockLabwareDefinition, @@ -141,7 +141,7 @@ describe('getRunLabwareRenderInfo', () => { const res = getRunLabwareRenderInfo( mockRunData, mockLabwareDefinitionsByUri, - deckDefFixture as any + standardDeckDef as any ) const labwareInfo = res[0] expect(labwareInfo).toBeTruthy() @@ -158,7 +158,7 @@ describe('getRunLabwareRenderInfo', () => { const res = getRunLabwareRenderInfo( mockRunData, mockLabwareDefinitionsByUri, - deckDefFixture as any + standardDeckDef as any ) expect(res).toHaveLength(1) // the offdeck labware still gets added because the mating surface doesn't exist for offdeck labware }) @@ -167,7 +167,7 @@ describe('getRunLabwareRenderInfo', () => { const res = getRunLabwareRenderInfo( mockRunData, mockLabwareDefinitionsByUri, - deckDefFixture as any + standardDeckDef as any ) expect(res).toHaveLength(2) const labwareInfo = res.find( @@ -176,7 +176,7 @@ describe('getRunLabwareRenderInfo', () => { expect(labwareInfo).toBeTruthy() expect(labwareInfo?.x).toEqual(0) expect(labwareInfo?.y).toEqual( - deckDefFixture.cornerOffsetFromOrigin[1] - + standardDeckDef.cornerOffsetFromOrigin[1] - mockLabwareDefinition.dimensions.yDimension ) }) @@ -193,7 +193,7 @@ describe('getRunLabwareRenderInfo', () => { const res = getRunLabwareRenderInfo( { labware: [mockBadSlotLabware] } as any, mockLabwareDefinitionsByUri, - deckDefFixture as any + standardDeckDef as any ) expect(res[0].x).toEqual(0) @@ -211,7 +211,7 @@ describe('getCurrentRunModuleRenderInfo', () => { it('returns run module render info with nested labware', () => { const res = getRunModuleRenderInfo( mockRunData, - deckDefFixture as any, + standardDeckDef as any, mockLabwareDefinitionsByUri ) const moduleInfo = res[0] @@ -232,7 +232,7 @@ describe('getCurrentRunModuleRenderInfo', () => { const res = getRunModuleRenderInfo( mockRunDataNoNesting, - deckDefFixture as any, + standardDeckDef as any, mockLabwareDefinitionsByUri ) @@ -249,7 +249,7 @@ describe('getCurrentRunModuleRenderInfo', () => { const res = getRunModuleRenderInfo( mockRunDataWithTC, - deckDefFixture as any, + standardDeckDef as any, mockLabwareDefinitionsByUri ) @@ -274,7 +274,7 @@ describe('getCurrentRunModuleRenderInfo', () => { const res = getRunModuleRenderInfo( mockRunDataWithBadModuleSlot, - deckDefFixture as any, + standardDeckDef as any, mockLabwareDefinitionsByUri ) diff --git a/app/src/organisms/InterventionModal/index.tsx b/app/src/organisms/InterventionModal/index.tsx index b5b23666ba5..39c6c3cb4d7 100644 --- a/app/src/organisms/InterventionModal/index.tsx +++ b/app/src/organisms/InterventionModal/index.tsx @@ -35,6 +35,7 @@ import { MoveLabwareInterventionContent } from './MoveLabwareInterventionContent import type { RunCommandSummary, RunData } from '@opentrons/api-client' import type { IconName } from '@opentrons/components' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' +import { useRobotType } from '../Devices/hooks' const LEARN_ABOUT_MANUAL_STEPS_URL = 'https://support.opentrons.com/s/article/Manual-protocol-steps' @@ -108,6 +109,7 @@ export function InterventionModal({ const { t } = useTranslation(['protocol_command_text', 'protocol_info']) const isOnDevice = useSelector(getIsOnDevice) + const robotType = useRobotType(robotName) const childContent = React.useMemo(() => { if ( command.commandType === 'waitForResume' || @@ -122,7 +124,7 @@ export function InterventionModal({ } else if (command.commandType === 'moveLabware') { return ( ) diff --git a/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts b/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts index 12718b07411..1634e12cc2b 100644 --- a/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts +++ b/app/src/organisms/InterventionModal/utils/getRunLabwareRenderInfo.ts @@ -1,4 +1,7 @@ -import { getSlotHasMatingSurfaceUnitVector } from '@opentrons/shared-data' +import { + getPositionFromSlotId, + getSlotHasMatingSurfaceUnitVector, +} from '@opentrons/shared-data' import type { RunData } from '@opentrons/api-client' import type { @@ -36,20 +39,22 @@ export function getRunLabwareRenderInfo( } if (location !== 'offDeck') { - const slotName = location.slotName - const slotPosition = - deckDef.locations.orderedSlots.find(slot => slot.id === slotName) - ?.position ?? [] + const slotName = + 'addressableAreaName' in location + ? location.addressableAreaName + : location.slotName + const slotPosition = getPositionFromSlotId(slotName, deckDef) const slotHasMatingSurfaceVector = getSlotHasMatingSurfaceUnitVector( deckDef, slotName ) + return slotHasMatingSurfaceVector ? [ ...acc, { - x: slotPosition[0] ?? 0, - y: slotPosition[1] ?? 0, + x: slotPosition?.[0] ?? 0, + y: slotPosition?.[1] ?? 0, labwareId: labware.id, labwareDef, }, diff --git a/app/src/organisms/InterventionModal/utils/getRunModuleRenderInfo.ts b/app/src/organisms/InterventionModal/utils/getRunModuleRenderInfo.ts index 0413f60db7e..46c8a04869e 100644 --- a/app/src/organisms/InterventionModal/utils/getRunModuleRenderInfo.ts +++ b/app/src/organisms/InterventionModal/utils/getRunModuleRenderInfo.ts @@ -1,4 +1,8 @@ -import { SPAN7_8_10_11_SLOT, getModuleDef2 } from '@opentrons/shared-data' +import { + SPAN7_8_10_11_SLOT, + getModuleDef2, + getPositionFromSlotId, +} from '@opentrons/shared-data' import type { RunData } from '@opentrons/api-client' import type { @@ -37,16 +41,14 @@ export function getRunModuleRenderInfo( if (slotName === SPAN7_8_10_11_SLOT) { slotName = '7' } - const slotPosition = - deckDef.locations.orderedSlots.find(slot => slot.id === slotName) - ?.position ?? [] + const slotPosition = getPositionFromSlotId(slotName, deckDef) return [ ...acc, { moduleId: module.id, - x: slotPosition[0] ?? 0, - y: slotPosition[1] ?? 0, + x: slotPosition?.[0] ?? 0, + y: slotPosition?.[1] ?? 0, moduleDef, nestedLabwareDef, nestedLabwareId: nestedLabware?.id ?? null, diff --git a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx index 5381c23aed5..48f9353147e 100644 --- a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx @@ -1,15 +1,18 @@ import * as React from 'react' import { Trans, useTranslation } from 'react-i18next' import { RESPONSIVENESS, SPACING, TYPOGRAPHY } from '@opentrons/components' -import { css } from 'styled-components' -import { StyledText } from '../../atoms/text' -import { RobotMotionLoader } from './RobotMotionLoader' +import { useInstrumentsQuery } from '@opentrons/react-api-client' import { CompletedProtocolAnalysis, getPipetteNameSpecs, } from '@opentrons/shared-data' +import { css } from 'styled-components' +import { StyledText } from '../../atoms/text' +import { ProbeNotAttached } from '../PipetteWizardFlows/ProbeNotAttached' +import { RobotMotionLoader } from './RobotMotionLoader' import attachProbe1 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_1.webm' import attachProbe8 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_8.webm' +import attachProbe96 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_96.webm' import { useChainRunCommands } from '../../resources/runs/hooks' import { GenericWizardTile } from '../../molecules/GenericWizardTile' @@ -19,7 +22,7 @@ import type { RegisterPositionAction, WorkingOffset, } from './types' -import type { LabwareOffset } from '@opentrons/api-client' +import type { LabwareOffset, PipetteData } from '@opentrons/api-client' interface AttachProbeProps extends AttachProbeStep { protocolData: CompletedProtocolAnalysis @@ -31,7 +34,9 @@ interface AttachProbeProps extends AttachProbeStep { existingOffsets: LabwareOffset[] handleJog: Jog isRobotMoving: boolean + isOnDevice: boolean } + export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { @@ -41,43 +46,97 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { chainRunCommands, isRobotMoving, setFatalError, + isOnDevice, } = props + const [isPending, setIsPending] = React.useState(false) + const [showUnableToDetect, setShowUnableToDetect] = React.useState( + false + ) const pipette = protocolData.pipettes.find(p => p.id === pipetteId) const pipetteName = pipette?.pipetteName const pipetteChannels = pipetteName != null ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 : 1 - const pipetteMount = pipette?.mount - if (pipetteName == null || pipetteMount == null) return null + let probeVideoSrc = attachProbe1 + let probeLocation = '' + if (pipetteChannels === 8) { + probeLocation = t('backmost') + probeVideoSrc = attachProbe8 + } else if (pipetteChannels === 96) { + probeLocation = t('ninety_six_probe_location') + probeVideoSrc = attachProbe96 + } - const pipetteZMotorAxis: 'leftZ' | 'rightZ' = - pipetteMount === 'left' ? 'leftZ' : 'rightZ' + const pipetteMount = pipette?.mount + const { refetch, data: attachedInstrumentsData } = useInstrumentsQuery({ + enabled: false, + onSettled: () => { + setIsPending(false) + }, + }) + const attachedPipette = attachedInstrumentsData?.data.find( + (instrument): instrument is PipetteData => + instrument.ok && instrument.mount === pipetteMount + ) + const is96Channel = attachedPipette?.data.channels === 96 - const handleProbeAttached = (): void => { + React.useEffect(() => { + // move into correct position for probe attach on mount chainRunCommands( [ { - commandType: 'retractAxis' as const, + commandType: 'calibration/moveToMaintenancePosition' as const, params: { - axis: pipetteZMotorAxis, + mount: pipetteMount ?? 'left', }, }, - { - commandType: 'retractAxis' as const, - params: { axis: 'x' }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'y' }, - }, ], false - ) - .then(() => proceed()) - .catch((e: Error) => { - setFatalError( - `AttachProbe failed to move to safe location after probe attach with message: ${e.message}` - ) + ).catch(error => setFatalError(error.message)) + }, []) + + if (pipetteName == null || pipetteMount == null) return null + + const pipetteZMotorAxis: 'leftZ' | 'rightZ' = + pipetteMount === 'left' ? 'leftZ' : 'rightZ' + + const handleProbeAttached = (): void => { + setIsPending(true) + refetch() + .then(() => { + if (is96Channel || attachedPipette?.state?.tipDetected) { + chainRunCommands( + [ + { commandType: 'home', params: { axes: [pipetteZMotorAxis] } }, + { + commandType: 'retractAxis' as const, + params: { + axis: pipetteZMotorAxis, + }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'x' }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'y' }, + }, + ], + false + ) + .then(() => proceed()) + .catch((e: Error) => { + setFatalError( + `AttachProbe failed to move to safe location after probe attach with message: ${e.message}` + ) + }) + } else { + setShowUnableToDetect(true) + } + }) + .catch(error => { + setFatalError(error.message) }) } @@ -85,11 +144,19 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { return ( ) + else if (showUnableToDetect) + return ( + + ) return ( { loop={true} controls={false} > - 1 ? attachProbe8 : attachProbe1} /> + } bodyText={ - pipetteChannels > 1 ? ( + , - block: , + bold: , }} /> - ) : ( - {t('install_probe')} - ) + } - proceedButtonText={t('begin_calibration')} + proceedButtonText={i18n.format(t('shared:continue'), 'capitalize')} proceed={handleProbeAttached} /> ) diff --git a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx index 7cfb76e79d0..97fe3137690 100644 --- a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx @@ -55,6 +55,7 @@ interface CheckItemProps extends Omit { handleJog: Jog isRobotMoving: boolean robotType: RobotType + shouldUseMetalProbe: boolean } export const CheckItem = (props: CheckItemProps): JSX.Element | null => { const { @@ -73,6 +74,7 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { existingOffsets, setFatalError, robotType, + shouldUseMetalProbe, } = props const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const isOnDevice = useSelector(getIsOnDevice) @@ -431,9 +433,17 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { t={t} i18nKey={ isOnDevice - ? 'ensure_nozzle_is_above_tip_odd' - : 'ensure_nozzle_is_above_tip_desktop' + ? 'ensure_nozzle_position_odd' + : 'ensure_nozzle_position_desktop' } + values={{ + tip_type: shouldUseMetalProbe + ? t('calibration_probe') + : t('pipette_nozzle'), + item_location: isTiprack + ? t('check_tip_location') + : t('check_well_location'), + }} components={{ block: , bold: }} /> } @@ -444,6 +454,7 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { handleJog={handleJog} initialPosition={initialPosition} existingOffset={existingOffset} + shouldUseMetalProbe={shouldUseMetalProbe} /> ) : ( { })} body={ } labwareDef={labwareDef} diff --git a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx index ab1c4f4898f..a1681d90e17 100644 --- a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx @@ -10,6 +10,7 @@ import { } from '@opentrons/shared-data' import detachProbe1 from '../../assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_1.webm' import detachProbe8 from '../../assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_8.webm' +import detachProbe96 from '../../assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_96.webm' import { useChainRunCommands } from '../../resources/runs/hooks' import { GenericWizardTile } from '../../molecules/GenericWizardTile' @@ -47,7 +48,29 @@ export const DetachProbe = (props: DetachProbeProps): JSX.Element | null => { const pipetteName = pipette?.pipetteName const pipetteChannels = pipetteName != null ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 : 1 + let probeVideoSrc = detachProbe1 + if (pipetteChannels === 8) { + probeVideoSrc = detachProbe8 + } else if (pipetteChannels === 96) { + probeVideoSrc = detachProbe96 + } const pipetteMount = pipette?.mount + + React.useEffect(() => { + // move into correct position for probe detach on mount + chainRunCommands( + [ + { + commandType: 'calibration/moveToMaintenancePosition' as const, + params: { + mount: pipetteMount ?? 'left', + }, + }, + ], + false + ).catch(error => setFatalError(error.message)) + }, []) + if (pipetteName == null || pipetteMount == null) return null const pipetteZMotorAxis: 'leftZ' | 'rightZ' = @@ -76,7 +99,7 @@ export const DetachProbe = (props: DetachProbeProps): JSX.Element | null => { .then(() => proceed()) .catch((e: Error) => { setFatalError( - `DetachProbe failed to move to safe location after probe attach with message: ${e.message}` + `DetachProbe failed to move to safe location after probe detach with message: ${e.message}` ) }) } @@ -101,7 +124,7 @@ export const DetachProbe = (props: DetachProbeProps): JSX.Element | null => { loop={true} controls={false} > - 1 ? detachProbe8 : detachProbe1} /> + } bodyText={ diff --git a/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx index 5bdb7911af7..7a86442779b 100644 --- a/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx +++ b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx @@ -16,23 +16,22 @@ import { RESPONSIVENESS, SecondaryButton, JUSTIFY_FLEX_END, + TEXT_ALIGN_CENTER, } from '@opentrons/components' import { StyledText } from '../../atoms/text' -import { NeedHelpLink } from '../CalibrationPanels' import { useSelector } from 'react-redux' import { getIsOnDevice } from '../../redux/config' import { SmallButton } from '../../atoms/buttons' -const LPC_HELP_LINK_URL = - 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' interface ExitConfirmationProps { onGoBack: () => void onConfirmExit: () => void + shouldUseMetalProbe: boolean } export const ExitConfirmation = (props: ExitConfirmationProps): JSX.Element => { const { i18n, t } = useTranslation(['labware_position_check', 'shared']) - const { onGoBack, onConfirmExit } = props + const { onGoBack, onConfirmExit, shouldUseMetalProbe } = props const isOnDevice = useSelector(getIsOnDevice) return ( { flexDirection={DIRECTION_COLUMN} justifyContent={JUSTIFY_CENTER} alignItems={ALIGN_CENTER} + paddingX={SPACING.spacing32} > - {t('exit_screen_title')} - - {t('exit_screen_subtitle')} - + {isOnDevice ? ( + <> + + {shouldUseMetalProbe + ? t('remove_probe_before_exit') + : t('exit_screen_title')} + + + + {t('exit_screen_subtitle')} + + + + ) : ( + <> + + {shouldUseMetalProbe + ? t('remove_probe_before_exit') + : t('exit_screen_title')} + + + {t('exit_screen_subtitle')} + + + )} {isOnDevice ? ( { /> @@ -77,7 +102,6 @@ export const ExitConfirmation = (props: ExitConfirmationProps): JSX.Element => { justifyContent={JUSTIFY_SPACE_BETWEEN} alignItems={ALIGN_CENTER} > - {t('shared:go_back')} @@ -86,7 +110,9 @@ export const ExitConfirmation = (props: ExitConfirmationProps): JSX.Element => { onClick={onConfirmExit} textTransform={TYPOGRAPHY.textTransformCapitalize} > - {t('shared:exit')} + {shouldUseMetalProbe + ? t('remove_calibration_probe') + : i18n.format(t('shared:exit'), 'capitalize')}
@@ -97,8 +123,23 @@ export const ExitConfirmation = (props: ExitConfirmationProps): JSX.Element => { const ConfirmationHeader = styled.h1` margin-top: ${SPACING.spacing24}; - ${TYPOGRAPHY.h1Default} + ${TYPOGRAPHY.level3HeaderSemiBold} @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { ${TYPOGRAPHY.level4HeaderSemiBold} } ` + +const ConfirmationHeaderODD = styled.h1` + margin-top: ${SPACING.spacing24}; + ${TYPOGRAPHY.level3HeaderBold} + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.level4HeaderSemiBold} + } +` +const ConfirmationBodyODD = styled.h1` + ${TYPOGRAPHY.level4HeaderRegular} + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.level4HeaderRegular} + } + color: ${COLORS.darkBlack70}; +` diff --git a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx index b0e112154fe..d5cf8dc0ee6 100644 --- a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx @@ -34,6 +34,7 @@ import { css } from 'styled-components' import { Portal } from '../../../App/portal' import { LegacyModalShell } from '../../../molecules/LegacyModal' import { SmallButton } from '../../../atoms/buttons' +import { CALIBRATION_PROBE } from '../../PipetteWizardFlows/constants' import { TerseOffsetTable } from '../ResultsSummary' import { getLabwareDefinitionsFromCommands } from '../utils/labware' @@ -52,6 +53,7 @@ export const IntroScreen = (props: { isRobotMoving: boolean existingOffsets: LabwareOffset[] protocolName: string + shouldUseMetalProbe: boolean }): JSX.Element | null => { const { proceed, @@ -61,6 +63,7 @@ export const IntroScreen = (props: { setFatalError, existingOffsets, protocolName, + shouldUseMetalProbe, } = props const isOnDevice = useSelector(getIsOnDevice) const { t, i18n } = useTranslation(['labware_position_check', 'shared']) @@ -74,6 +77,19 @@ export const IntroScreen = (props: { ) }) } + const requiredEquipmentList = [ + { + loadName: t('all_modules_and_labware_from_protocol', { + protocol_name: protocolName, + }), + displayName: t('all_modules_and_labware_from_protocol', { + protocol_name: protocolName, + }), + }, + ] + if (shouldUseMetalProbe) { + requiredEquipmentList.push(CALIBRATION_PROBE) + } if (isRobotMoving) { return ( @@ -91,18 +107,7 @@ export const IntroScreen = (props: { /> } rightElement={ - + } footer={ diff --git a/app/src/organisms/LabwarePositionCheck/JogToWell.tsx b/app/src/organisms/LabwarePositionCheck/JogToWell.tsx index c634a2edb3e..4e91343b85a 100644 --- a/app/src/organisms/LabwarePositionCheck/JogToWell.tsx +++ b/app/src/organisms/LabwarePositionCheck/JogToWell.tsx @@ -29,6 +29,8 @@ import { import levelWithTip from '../../assets/images/lpc_level_with_tip.svg' import levelWithLabware from '../../assets/images/lpc_level_with_labware.svg' +import levelProbeWithTip from '../../assets/images/lpc_level_probe_with_tip.svg' +import levelProbeWithLabware from '../../assets/images/lpc_level_probe_with_labware.svg' import { getIsOnDevice } from '../../redux/config' import { Portal } from '../../App/portal' import { LegacyModalShell } from '../../molecules/LegacyModal' @@ -57,6 +59,7 @@ interface JogToWellProps { body: React.ReactNode initialPosition: VectorOffset existingOffset: VectorOffset + shouldUseMetalProbe: boolean } export const JogToWell = (props: JogToWellProps): JSX.Element | null => { const { t } = useTranslation(['labware_position_check', 'shared']) @@ -70,6 +73,7 @@ export const JogToWell = (props: JogToWellProps): JSX.Element | null => { handleJog, initialPosition, existingOffset, + shouldUseMetalProbe, } = props const [joggedPosition, setJoggedPosition] = React.useState( @@ -109,6 +113,10 @@ export const JogToWell = (props: JogToWellProps): JSX.Element | null => { getVectorDifference(joggedPosition, initialPosition) ) const isTipRack = getIsTiprack(labwareDef) + let levelSrc = isTipRack ? levelWithTip : levelWithLabware + if (shouldUseMetalProbe) { + levelSrc = isTipRack ? levelProbeWithTip : levelProbeWithLabware + } return ( { {`level diff --git a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx index 4a96d8bc2e3..8df5ad42bfa 100644 --- a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx +++ b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx @@ -261,6 +261,8 @@ export const LabwarePositionCheckComponent = ( const currentStep = LPCSteps?.[currentStepIndex] if (currentStep == null) return null + const protocolHasModules = protocolData.modules.length > 0 + const handleJog = ( axis: Axis, dir: Sign, @@ -336,6 +338,7 @@ export const LabwarePositionCheckComponent = ( ) } else if (currentStep.section === 'BEFORE_BEGINNING') { @@ -344,6 +347,7 @@ export const LabwarePositionCheckComponent = ( {...movementStepProps} {...{ existingOffsets }} protocolName={protocolName} + shouldUseMetalProbe={shouldUseMetalProbe} /> ) } else if ( @@ -351,13 +355,32 @@ export const LabwarePositionCheckComponent = ( currentStep.section === 'CHECK_TIP_RACKS' || currentStep.section === 'CHECK_LABWARE' ) { - modalContent = + modalContent = ( + + ) } else if (currentStep.section === 'ATTACH_PROBE') { - modalContent = + modalContent = ( + + ) } else if (currentStep.section === 'DETACH_PROBE') { modalContent = } else if (currentStep.section === 'PICK_UP_TIP') { - modalContent = + modalContent = ( + + ) } else if (currentStep.section === 'RETURN_TIP') { modalContent = ( { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) @@ -67,6 +69,8 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { setFatalError, adapterId, robotType, + protocolHasModules, + currentStepIndex, } = props const [showTipConfirmation, setShowTipConfirmation] = React.useState(false) const isOnDevice = useSelector(getIsOnDevice) @@ -87,7 +91,10 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { ) const labwareDisplayName = getLabwareDisplayName(labwareDef) const instructions = [ - t('clear_all_slots'), + ...(protocolHasModules && currentStepIndex === 1 + ? [t('place_modules')] + : []), + isOnDevice ? t('clear_all_slots_odd') : t('clear_all_slots'), { t={t} i18nKey={ isOnDevice - ? 'ensure_nozzle_is_above_tip_odd' - : 'ensure_nozzle_is_above_tip_desktop' + ? 'ensure_nozzle_position_odd' + : 'ensure_nozzle_position_desktop' } components={{ block: , bold: }} + values={{ + tip_type: t('pipette_nozzle'), + item_location: t('check_tip_location'), + }} /> } labwareDef={labwareDef} @@ -413,6 +424,7 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { handleJog={handleJog} initialPosition={initialPosition} existingOffset={existingOffset} + shouldUseMetalProbe={false} /> ) : ( { labwareLocation: location, definition: labwareDef, }, - ]} + ].filter( + () => !('moduleModel' in location && location.moduleModel != null) + )} deckConfig={deckConfig} /> diff --git a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx b/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx index c751d65a3b8..a0c31074a5c 100644 --- a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx +++ b/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx @@ -20,6 +20,8 @@ import { import { getDisplayLocation } from './utils/getDisplayLocation' import { RobotMotionLoader } from './RobotMotionLoader' import { PrepareSpace } from './PrepareSpace' +import { useSelector } from 'react-redux' +import { getIsOnDevice } from '../../redux/config' import type { VectorOffset } from '@opentrons/api-client' import type { ReturnTipStep } from './types' @@ -48,6 +50,8 @@ export const ReturnTip = (props: ReturnTipProps): JSX.Element | null => { adapterId, } = props + const isOnDevice = useSelector(getIsOnDevice) + const labwareDef = getLabwareDef(labwareId, protocolData) if (labwareDef == null) return null @@ -60,7 +64,7 @@ export const ReturnTip = (props: ReturnTipProps): JSX.Element | null => { const labwareDisplayName = getLabwareDisplayName(labwareDef) const instructions = [ - t('clear_all_slots'), + isOnDevice ? t('clear_all_slots_odd') : t('clear_all_slots'), - + { existingOffsets: mockExistingOffsets, isRobotMoving: false, robotType: FLEX_ROBOT_TYPE, + shouldUseMetalProbe: false, } }) afterEach(() => { diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/ExitConfirmation.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/ExitConfirmation.test.tsx index 61552a0f1cb..3263e098e22 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/ExitConfirmation.test.tsx +++ b/app/src/organisms/LabwarePositionCheck/__tests__/ExitConfirmation.test.tsx @@ -17,6 +17,7 @@ describe('ExitConfirmation', () => { props = { onGoBack: jest.fn(), onConfirmExit: jest.fn(), + shouldUseMetalProbe: false, } }) afterEach(() => { @@ -29,14 +30,26 @@ describe('ExitConfirmation', () => { getByText( 'If you exit now, all labware offsets will be discarded. This cannot be undone.' ) - getByRole('button', { name: 'exit' }) + getByRole('button', { name: 'Exit' }) getByRole('button', { name: 'Go back' }) }) it('should invoke callback props when ctas are clicked', () => { const { getByRole } = render(props) getByRole('button', { name: 'Go back' }).click() expect(props.onGoBack).toHaveBeenCalled() - getByRole('button', { name: 'exit' }).click() + getByRole('button', { name: 'Exit' }).click() expect(props.onConfirmExit).toHaveBeenCalled() }) + it('should render correct copy for golden tip LPC', () => { + const { getByText, getByRole } = render({ + ...props, + shouldUseMetalProbe: true, + }) + getByText('Remove the calibration probe before exiting') + getByText( + 'If you exit now, all labware offsets will be discarded. This cannot be undone.' + ) + getByRole('button', { name: 'Remove calibration probe' }) + getByRole('button', { name: 'Go back' }) + }) }) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx index f6fc45a0a2c..b67107ec005 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx +++ b/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx @@ -62,6 +62,8 @@ describe('PickUpTip', () => { existingOffsets: mockExistingOffsets, isRobotMoving: false, robotType: FLEX_ROBOT_TYPE, + protocolHasModules: false, + currentStepIndex: 1, } mockUseProtocolMetaData.mockReturnValue({ robotType: 'OT-3 Standard' }) }) @@ -69,16 +71,46 @@ describe('PickUpTip', () => { jest.resetAllMocks() resetAllWhenMocks() }) - it('renders correct copy when preparing space', () => { + it('renders correct copy when preparing space on desktop if protocol has modules', () => { + props.protocolHasModules = true const { getByText, getByRole } = render(props) getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) + getByText('Place modules on deck') getByText('Clear all deck slots of labware, leaving modules in place') getByText( matchTextWithSpans('Place a full Mock TipRack Definition into Slot D1') ) - getByRole('link', { name: 'Need help?' }) getByRole('button', { name: 'Confirm placement' }) }) + it('renders correct copy when preparing space on touchscreen if protocol has modules', () => { + mockGetIsOnDevice.mockReturnValue(true) + props.protocolHasModules = true + const { getByText, getByRole } = render(props) + getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) + getByText('Place modules on deck') + getByText('Clear all deck slots of labware') + getByText( + matchTextWithSpans('Place a full Mock TipRack Definition into Slot D1') + ) + }) + it('renders correct copy when preparing space on desktop if protocol has no modules', () => { + const { getByText, getByRole } = render(props) + getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) + getByText('Clear all deck slots of labware, leaving modules in place') + getByText( + matchTextWithSpans('Place a full Mock TipRack Definition into Slot D1') + ) + getByRole('button', { name: 'Confirm placement' }) + }) + it('renders correct copy when preparing space on touchscreen if protocol has no modules', () => { + mockGetIsOnDevice.mockReturnValue(true) + const { getByText, getByRole } = render(props) + getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) + getByText('Clear all deck slots of labware') + getByText( + matchTextWithSpans('Place a full Mock TipRack Definition into Slot D1') + ) + }) it('renders correct copy when confirming position on desktop', () => { const { getByText, getByRole } = render({ ...props, diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx index 465609db8c4..037d171579a 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx +++ b/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx @@ -7,12 +7,17 @@ import { ReturnTip } from '../ReturnTip' import { SECTIONS } from '../constants' import { mockCompletedAnalysis } from '../__fixtures__' import { useProtocolMetadata } from '../../Devices/hooks' +import { getIsOnDevice } from '../../../redux/config' jest.mock('../../Devices/hooks') +jest.mock('../../../redux/config') const mockUseProtocolMetaData = useProtocolMetadata as jest.MockedFunction< typeof useProtocolMetadata > +const mockGetIsOnDevice = getIsOnDevice as jest.MockedFunction< + typeof getIsOnDevice +> const matchTextWithSpans: (text: string) => MatcherFunction = ( text: string @@ -37,6 +42,7 @@ describe('ReturnTip', () => { beforeEach(() => { mockChainRunCommands = jest.fn().mockImplementation(() => Promise.resolve()) + mockGetIsOnDevice.mockReturnValue(false) props = { section: SECTIONS.RETURN_TIP, pipetteId: mockCompletedAnalysis.pipettes[0].id, @@ -55,7 +61,7 @@ describe('ReturnTip', () => { afterEach(() => { jest.restoreAllMocks() }) - it('renders correct copy', () => { + it('renders correct copy on desktop', () => { const { getByText, getByRole } = render(props) getByRole('heading', { name: 'Return tip rack to Slot D1' }) getByText('Clear all deck slots of labware, leaving modules in place') @@ -66,6 +72,17 @@ describe('ReturnTip', () => { ) getByRole('link', { name: 'Need help?' }) }) + it('renders correct copy on device', () => { + mockGetIsOnDevice.mockReturnValue(true) + const { getByText, getByRole } = render(props) + getByRole('heading', { name: 'Return tip rack to Slot D1' }) + getByText('Clear all deck slots of labware') + getByText( + matchTextWithSpans( + 'Place the Mock TipRack Definition that you used before back into Slot D1. The pipette will return tips to their original location in the rack.' + ) + ) + }) it('executes correct chained commands when CTA is clicked', async () => { const { getByRole } = render(props) await getByRole('button', { name: 'Confirm placement' }).click() diff --git a/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts b/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts index 4d0ece68dc0..16f8dcc4478 100644 --- a/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts +++ b/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts @@ -20,5 +20,11 @@ export function useMostRecentCompletedAnalysis( { enabled: protocolData != null } ) - return analysis ?? null + return analysis != null + ? { + ...analysis, + // NOTE: this is accounting for pre 7.1 robot-side protocol analysis that may not include the robotType key + robotType: analysis.robotType ?? protocolData?.data.robotType, + } + : null } diff --git a/app/src/organisms/LabwarePositionCheck/utils/labware.ts b/app/src/organisms/LabwarePositionCheck/utils/labware.ts index fdaa37ba2ee..bdd6f019aaf 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/labware.ts +++ b/app/src/organisms/LabwarePositionCheck/utils/labware.ts @@ -192,7 +192,10 @@ export const getLabwareIdsInOrder = ( ).location.slotName } } else { - slot = loc.slotName + slot = + 'addressableAreaName' in loc + ? loc.addressableAreaName + : loc.slotName } return [ ...innerAcc, diff --git a/app/src/organisms/ModuleCard/__tests__/hooks.test.tsx b/app/src/organisms/ModuleCard/__tests__/hooks.test.tsx index fc96e7ae506..0ec0c27de50 100644 --- a/app/src/organisms/ModuleCard/__tests__/hooks.test.tsx +++ b/app/src/organisms/ModuleCard/__tests__/hooks.test.tsx @@ -7,9 +7,7 @@ import { I18nextProvider } from 'react-i18next' import { renderHook } from '@testing-library/react-hooks' import { i18n } from '../../../i18n' import { useCreateLiveCommandMutation } from '@opentrons/react-api-client' -import { ModuleModel, ModuleType } from '@opentrons/shared-data' import heaterShakerCommandsWithResultsKey from '@opentrons/shared-data/protocol/fixtures/6/heaterShakerCommandsWithResultsKey.json' -import { getProtocolModulesInfo } from '../../Devices/ProtocolRun/utils/getProtocolModulesInfo' import { useCurrentRunId } from '../../ProtocolUpload/hooks' import { useIsRobotBusy, useRunStatuses } from '../../Devices/hooks' import { @@ -31,7 +29,6 @@ import type { Store } from 'redux' import type { State } from '../../../redux/types' jest.mock('@opentrons/react-api-client') -jest.mock('../../Devices/ProtocolRun/utils/getProtocolModulesInfo') jest.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') jest.mock('../../ProtocolUpload/hooks') jest.mock('../../Devices/hooks') @@ -39,9 +36,6 @@ jest.mock('../../Devices/hooks') const mockUseMostRecentCompletedAnalysis = useMostRecentCompletedAnalysis as jest.MockedFunction< typeof useMostRecentCompletedAnalysis > -const mockGetProtocolModulesInfo = getProtocolModulesInfo as jest.MockedFunction< - typeof getProtocolModulesInfo -> const mockUseLiveCommandMutation = useCreateLiveCommandMutation as jest.MockedFunction< typeof useCreateLiveCommandMutation @@ -608,49 +602,27 @@ describe('useModuleOverflowMenu', () => { }) }) -const mockHeaterShakerDefinition = { - moduleId: 'someHeaterShakerModule', - model: 'heaterShakerModuleV1' as ModuleModel, - type: 'heaterShakerModuleType' as ModuleType, - displayName: 'Heater Shaker Module', - labwareOffset: { x: 5, y: 5, z: 5 }, - cornerOffsetFromSlot: { x: 1, y: 1, z: 1 }, - dimensions: { - xDimension: 100, - yDimension: 100, - footprintXDimension: 50, - footprintYDimension: 50, - labwareInterfaceXDimension: 80, - labwareInterfaceYDimension: 120, - }, - twoDimensionalRendering: { children: [] }, -} - -const HEATER_SHAKER_MODULE_INFO = { - moduleId: 'heaterShakerModuleId', - x: 0, - y: 0, - z: 0, - moduleDef: mockHeaterShakerDefinition as any, - nestedLabwareDef: null, - nestedLabwareId: null, - nestedLabwareDisplayName: null, - protocolLoadOrder: 0, - slotName: '1', -} - describe('useIsHeaterShakerInProtocol', () => { const store: Store = createStore(jest.fn(), {}) beforeEach(() => { when(mockUseCurrentRunId).calledWith().mockReturnValue('1') store.dispatch = jest.fn() - mockGetProtocolModulesInfo.mockReturnValue([HEATER_SHAKER_MODULE_INFO]) when(mockUseMostRecentCompletedAnalysis) .calledWith('1') .mockReturnValue({ ...heaterShakerCommandsWithResultsKey, + modules: [ + { + id: 'fake_module_id', + model: 'heaterShakerModuleV1', + location: { + slotName: '1', + }, + serialNumber: 'fake_serial', + }, + ], labware: Object.keys(heaterShakerCommandsWithResultsKey.labware).map( id => ({ location: 'offDeck', @@ -677,8 +649,20 @@ describe('useIsHeaterShakerInProtocol', () => { }) it('should return false when a heater shaker is NOT in the protocol', () => { - mockGetProtocolModulesInfo.mockReturnValue([]) - + when(mockUseMostRecentCompletedAnalysis) + .calledWith('1') + .mockReturnValue({ + ...heaterShakerCommandsWithResultsKey, + modules: [], + labware: Object.keys(heaterShakerCommandsWithResultsKey.labware).map( + id => ({ + location: 'offDeck', + loadName: id, + definitionUrui: id, + id, + }) + ), + } as any) const wrapper: React.FunctionComponent<{}> = ({ children }) => ( {children} ) diff --git a/app/src/organisms/ModuleCard/hooks.tsx b/app/src/organisms/ModuleCard/hooks.tsx index eb5e835de7c..5f2f7622f2e 100644 --- a/app/src/organisms/ModuleCard/hooks.tsx +++ b/app/src/organisms/ModuleCard/hooks.tsx @@ -3,14 +3,11 @@ import { useCreateLiveCommandMutation } from '@opentrons/react-api-client' import { useTranslation } from 'react-i18next' import { useHoverTooltip } from '@opentrons/components' import { - getDeckDefFromRobotType, - getRobotTypeFromLoadedLabware, HEATERSHAKER_MODULE_TYPE, MAGNETIC_MODULE_TYPE, TEMPERATURE_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' -import { getProtocolModulesInfo } from '../Devices/ProtocolRun/utils/getProtocolModulesInfo' import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { MenuItem } from '../../atoms/MenuList/MenuItem' import { Tooltip } from '../../atoms/Tooltip' @@ -35,15 +32,9 @@ export function useIsHeaterShakerInProtocol(): boolean { const currentRunId = useCurrentRunId() const robotProtocolAnalysis = useMostRecentCompletedAnalysis(currentRunId) if (robotProtocolAnalysis == null) return false - const robotType = getRobotTypeFromLoadedLabware(robotProtocolAnalysis.labware) - const deckDef = getDeckDefFromRobotType(robotType) - const protocolModulesInfo = getProtocolModulesInfo( - robotProtocolAnalysis, - deckDef - ) - return protocolModulesInfo.some( - module => module.moduleDef.model === 'heaterShakerModuleV1' + return robotProtocolAnalysis.modules.some( + module => module.model === 'heaterShakerModuleV1' ) } interface LatchControls { diff --git a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx index eb78f2b63aa..107813567e2 100644 --- a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx +++ b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx @@ -66,7 +66,7 @@ export const SelectLocation = ( deckDef={deckDef} selectedLocation={{ slotName }} setSelectedLocation={loc => setSlotName(loc.slotName)} - disabledLocations={deckDef.locations.orderedSlots.reduce< + disabledLocations={deckDef.locations.addressableAreas.reduce< ModuleLocation[] >((acc, slot) => { if (availableSlotNames.some(slotName => slotName === slot.id)) diff --git a/app/src/organisms/ModuleWizardFlows/index.tsx b/app/src/organisms/ModuleWizardFlows/index.tsx index 65fa45f855e..5361380b226 100644 --- a/app/src/organisms/ModuleWizardFlows/index.tsx +++ b/app/src/organisms/ModuleWizardFlows/index.tsx @@ -60,7 +60,11 @@ export const ModuleWizardFlows = ( const isOnDevice = useSelector(getIsOnDevice) const { t } = useTranslation('module_wizard_flows') const attachedPipettes = useAttachedPipettesFromInstrumentsQuery() - const attachedPipette = attachedPipettes.left ?? attachedPipettes.right + const attachedPipette = + attachedPipettes.left?.data.calibratedOffset?.last_modified != null + ? attachedPipettes.left + : attachedPipettes.right + const moduleCalibrationSteps = getModuleCalibrationSteps() const availableSlotNames = FLEX_SLOT_NAMES_BY_MOD_TYPE[getModuleType(attachedModule.moduleModel)] ?? [] @@ -193,7 +197,11 @@ export const ModuleWizardFlows = ( continuePastCommandFailure ) } - if (currentStep == null || attachedPipette == null) return null + if ( + currentStep == null || + attachedPipette?.data.calibratedOffset?.last_modified == null + ) + return null const maintenanceRunId = maintenanceRunData?.data.id != null && diff --git a/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx b/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx index 881d8e04631..388b6420fae 100644 --- a/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx +++ b/app/src/organisms/PipetteWizardFlows/AttachProbe.tsx @@ -3,19 +3,13 @@ import { css } from 'styled-components' import { Trans, useTranslation } from 'react-i18next' import { Flex, - Btn, - PrimaryButton, TYPOGRAPHY, COLORS, SPACING, RESPONSIVENESS, - JUSTIFY_SPACE_BETWEEN, - ALIGN_FLEX_END, - ALIGN_CENTER, } from '@opentrons/components' import { LEFT, MotorAxes } from '@opentrons/shared-data' import { useInstrumentsQuery } from '@opentrons/react-api-client' -import { SmallButton } from '../../atoms/buttons' import { StyledText } from '../../atoms/text' import { GenericWizardTile } from '../../molecules/GenericWizardTile' import { SimpleWizardBody } from '../../molecules/SimpleWizardBody' @@ -25,6 +19,7 @@ import pipetteProbe8 from '../../assets/videos/pipette-wizard-flows/Pipette_Prob import probing96 from '../../assets/videos/pipette-wizard-flows/Pipette_Probing_96.webm' import { BODY_STYLE, SECTIONS, FLOWS } from './constants' import { getPipetteAnimations } from './utils' +import { ProbeNotAttached } from './ProbeNotAttached' import type { PipetteData } from '@opentrons/api-client' import type { PipetteWizardStepProps } from './types' @@ -43,32 +38,6 @@ const IN_PROGRESS_STYLE = css` } ` -const ALIGN_BUTTONS = css` - align-items: ${ALIGN_FLEX_END}; - - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - align-items: ${ALIGN_CENTER}; - } -` -const GO_BACK_BUTTON_STYLE = css` - ${TYPOGRAPHY.pSemiBold}; - color: ${COLORS.darkGreyEnabled}; - padding-left: ${SPACING.spacing32}; - - &:hover { - opacity: 70%; - } - - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; - font-size: ${TYPOGRAPHY.fontSize22}; - padding-left: 0rem; - &:hover { - opacity: 100%; - } - } -` - export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { const { proceed, @@ -89,7 +58,6 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { const [showUnableToDetect, setShowUnableToDetect] = React.useState( false ) - const [numberOfTryAgains, setNumberOfTryAgains] = React.useState(0) const pipetteId = attachedPipettes[mount]?.serialNumber const displayName = attachedPipettes[mount]?.displayName @@ -113,7 +81,7 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { setIsPending(true) refetch() .then(() => { - if (attachedPipette?.state?.tipDetected) { + if (is96Channel || attachedPipette?.state?.tipDetected) { chainRunCommands?.( [ { @@ -211,41 +179,12 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { ) else if (showUnableToDetect) return ( - 2 ? t('something_seems_wrong') : undefined - } - iconColor={COLORS.errorEnabled} - isSuccess={false} - > - - setShowUnableToDetect(false)}> - - {t('shared:go_back')} - - - {isOnDevice ? ( - { - setNumberOfTryAgains(numberOfTryAgains + 1) - handleOnClick() - }} - /> - ) : ( - - {t('try_again')} - - )} - - + ) return errorMessage != null ? ( diff --git a/app/src/organisms/PipetteWizardFlows/ProbeNotAttached.tsx b/app/src/organisms/PipetteWizardFlows/ProbeNotAttached.tsx new file mode 100644 index 00000000000..ffee8350cbc --- /dev/null +++ b/app/src/organisms/PipetteWizardFlows/ProbeNotAttached.tsx @@ -0,0 +1,101 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { + Flex, + Btn, + PrimaryButton, + RESPONSIVENESS, + SPACING, + TYPOGRAPHY, + COLORS, + JUSTIFY_SPACE_BETWEEN, + ALIGN_FLEX_END, + ALIGN_CENTER, +} from '@opentrons/components' +import { css } from 'styled-components' +import { SimpleWizardBody } from '../../molecules/SimpleWizardBody' +import { StyledText } from '../../atoms/text' +import { SmallButton } from '../../atoms/buttons' + +interface ProbeNotAttachedProps { + handleOnClick: () => void + setShowUnableToDetect: (ableToDetect: boolean) => void + isOnDevice: boolean + isPending: boolean +} + +export const ProbeNotAttached = ( + props: ProbeNotAttachedProps +): JSX.Element | null => { + const { t, i18n } = useTranslation(['pipette_wizard_flows', 'shared']) + const { isOnDevice, isPending, handleOnClick, setShowUnableToDetect } = props + const [numberOfTryAgains, setNumberOfTryAgains] = React.useState(0) + + return ( + 2 ? t('something_seems_wrong') : undefined} + iconColor={COLORS.errorEnabled} + isSuccess={false} + > + + setShowUnableToDetect(false)}> + + {t('shared:go_back')} + + + {isOnDevice ? ( + { + setNumberOfTryAgains(numberOfTryAgains + 1) + handleOnClick() + }} + /> + ) : ( + { + setNumberOfTryAgains(numberOfTryAgains + 1) + handleOnClick() + }} + > + {i18n.format(t('shared:try_again'), 'capitalize')} + + )} + + + ) +} + +const ALIGN_BUTTONS = css` + align-items: ${ALIGN_FLEX_END}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + align-items: ${ALIGN_CENTER}; + } +` +const GO_BACK_BUTTON_STYLE = css` + ${TYPOGRAPHY.pSemiBold}; + color: ${COLORS.darkGreyEnabled}; + padding-left: ${SPACING.spacing32}; + + &:hover { + opacity: 70%; + } + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; + font-size: ${TYPOGRAPHY.fontSize22}; + padding-left: 0rem; + &:hover { + opacity: 100%; + } + } +` diff --git a/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx b/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx index 6b87d735279..e4da0e001c7 100644 --- a/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx +++ b/app/src/organisms/ProtocolDetails/RobotConfigurationDetails.tsx @@ -13,10 +13,12 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + getCutoutDisplayName, getFixtureDisplayName, getModuleDisplayName, getModuleType, getPipetteNameSpecs, + SINGLE_SLOT_FIXTURES, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' @@ -28,17 +30,18 @@ import { getSlotsForThermocycler } from './utils' import type { LoadModuleRunTimeCommand, - LoadFixtureRunTimeCommand, PipetteName, RobotType, + SingleSlotCutoutFixtureId, } from '@opentrons/shared-data' +import type { CutoutConfigProtocolSpec } from '../../resources/deck_configuration/utils' interface RobotConfigurationDetailsProps { leftMountPipetteName: PipetteName | null rightMountPipetteName: PipetteName | null extensionInstrumentName: string | null requiredModuleDetails: LoadModuleRunTimeCommand[] - requiredFixtureDetails: LoadFixtureRunTimeCommand[] + requiredFixtureDetails: CutoutConfigProtocolSpec[] isLoading: boolean robotType: RobotType | null } @@ -110,6 +113,14 @@ export const RobotConfigurationDetails = ( emptyText ) + // filter out single slot fixtures + const nonStandardRequiredFixtureDetails = requiredFixtureDetails.filter( + fixture => + !SINGLE_SLOT_FIXTURES.includes( + fixture.cutoutFixtureId as SingleSlotCutoutFixtureId + ) + ) + return ( ) })} - {requiredFixtureDetails.map((fixture, index) => { + {nonStandardRequiredFixtureDetails.map((fixture, index) => { return ( - {getFixtureDisplayName(fixture.params.loadName)} + {getFixtureDisplayName(fixture.cutoutFixtureId)} } /> diff --git a/app/src/organisms/ProtocolDetails/index.tsx b/app/src/organisms/ProtocolDetails/index.tsx index da13f2147e0..66917dcc1bd 100644 --- a/app/src/organisms/ProtocolDetails/index.tsx +++ b/app/src/organisms/ProtocolDetails/index.tsx @@ -37,12 +37,8 @@ import { parseInitialLoadedLabwareBySlot, parseInitialLoadedLabwareByModuleId, parseInitialLoadedLabwareByAdapter, - parseInitialLoadedFixturesByCutout, } from '@opentrons/api-client' -import { - WASTE_CHUTE_LOAD_NAME, - getGripperDisplayName, -} from '@opentrons/shared-data' +import { getGripperDisplayName } from '@opentrons/shared-data' import { Portal } from '../../App/portal' import { Divider } from '../../atoms/structure' @@ -58,6 +54,7 @@ import { analyzeProtocol, } from '../../redux/protocol-storage' import { useFeatureFlag } from '../../redux/config' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import { ChooseRobotToRunProtocolSlideout } from '../ChooseRobotToRunProtocolSlideout' import { SendProtocolToOT3Slideout } from '../SendProtocolToOT3Slideout' import { ProtocolAnalysisFailure } from '../ProtocolAnalysisFailure' @@ -72,11 +69,7 @@ import { ProtocolLabwareDetails } from './ProtocolLabwareDetails' import { ProtocolLiquidsDetails } from './ProtocolLiquidsDetails' import { RobotConfigurationDetails } from './RobotConfigurationDetails' -import type { - JsonConfig, - LoadFixtureRunTimeCommand, - PythonConfig, -} from '@opentrons/shared-data' +import type { JsonConfig, PythonConfig } from '@opentrons/shared-data' import type { StoredProtocolData } from '../../redux/protocol-storage' import type { State, Dispatch } from '../../redux/types' @@ -237,29 +230,9 @@ export function ProtocolDetails( ? map(parseInitialLoadedModulesBySlot(mostRecentAnalysis.commands)) : [] - // TODO: IMMEDIATELY remove stubbed fixture as soon as PE supports loadFixture - const STUBBED_LOAD_FIXTURE: LoadFixtureRunTimeCommand = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'D3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - } - const requiredFixtureDetails = - mostRecentAnalysis?.commands != null - ? [ - ...map( - parseInitialLoadedFixturesByCutout(mostRecentAnalysis.commands) - ), - STUBBED_LOAD_FIXTURE, - ] - : [] + const requiredFixtureDetails = getSimplestDeckConfigForProtocolCommands( + mostRecentAnalysis?.commands ?? [] + ) const requiredLabwareDetails = mostRecentAnalysis != null diff --git a/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx b/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx index 3b9b63a492f..b8382ac1ef2 100644 --- a/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx +++ b/app/src/organisms/ProtocolSetupDeckConfiguration/__tests__/ProtocolSetupDeckConfiguration.test.tsx @@ -1,23 +1,19 @@ import * as React from 'react' import { when, resetAllWhenMocks } from 'jest-when' -import { renderWithProviders, DeckConfigurator } from '@opentrons/components' -import { - useUpdateDeckConfigurationMutation, - useCreateDeckConfigurationMutation, -} from '@opentrons/react-api-client' +import { renderWithProviders, BaseDeck } from '@opentrons/components' +import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' import { i18n } from '../../../i18n' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { ProtocolSetupDeckConfiguration } from '..' -jest.mock('@opentrons/components/src/hardware-sim/DeckConfigurator/index') +jest.mock('@opentrons/components/src/hardware-sim/BaseDeck/index') jest.mock('@opentrons/react-api-client') jest.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') const mockSetSetupScreen = jest.fn() const mockUpdateDeckConfiguration = jest.fn() -const mockCreateDeckConfiguration = jest.fn() const PROTOCOL_DETAILS = { displayName: 'fake protocol', protocolData: [], @@ -25,18 +21,13 @@ const PROTOCOL_DETAILS = { robotType: 'OT-3 Standard' as const, } -const mockDeckConfigurator = DeckConfigurator as jest.MockedFunction< - typeof DeckConfigurator -> const mockUseMostRecentCompletedAnalysis = useMostRecentCompletedAnalysis as jest.MockedFunction< typeof useMostRecentCompletedAnalysis > const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction< typeof useUpdateDeckConfigurationMutation > -const mockUseCreateDeckConfigurationMutation = useCreateDeckConfigurationMutation as jest.MockedFunction< - typeof useCreateDeckConfigurationMutation -> +const mockBaseDeck = BaseDeck as jest.MockedFunction const render = ( props: React.ComponentProps @@ -51,21 +42,18 @@ describe('ProtocolSetupDeckConfiguration', () => { beforeEach(() => { props = { - fixtureLocation: 'D3', + cutoutId: 'cutoutD3', runId: 'mockRunId', setSetupScreen: mockSetSetupScreen, providedFixtureOptions: [], } - mockDeckConfigurator.mockReturnValue(
mock DeckConfigurator
) + mockBaseDeck.mockReturnValue(
mock BaseDeck
) when(mockUseMostRecentCompletedAnalysis) .calledWith('mockRunId') .mockReturnValue(PROTOCOL_DETAILS.protocolData as any) mockUseUpdateDeckConfigurationMutation.mockReturnValue({ updateDeckConfiguration: mockUpdateDeckConfiguration, } as any) - mockUseCreateDeckConfigurationMutation.mockReturnValue({ - createDeckConfiguration: mockCreateDeckConfiguration, - } as any) }) afterEach(() => { @@ -75,7 +63,7 @@ describe('ProtocolSetupDeckConfiguration', () => { it('should render text, button, and DeckConfigurator', () => { const [{ getByText }] = render(props) getByText('Deck configuration') - getByText('mock DeckConfigurator') + getByText('mock BaseDeck') getByText('Confirm') }) @@ -88,6 +76,6 @@ describe('ProtocolSetupDeckConfiguration', () => { it('should call a mock function when tapping confirm button', () => { const [{ getByText }] = render(props) getByText('Confirm').click() - expect(mockCreateDeckConfiguration).toHaveBeenCalled() + expect(mockUpdateDeckConfiguration).toHaveBeenCalled() }) }) diff --git a/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx b/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx index 8646070aa09..435498c53d9 100644 --- a/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx +++ b/app/src/organisms/ProtocolSetupDeckConfiguration/index.tsx @@ -2,39 +2,38 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { - DeckConfigurator, + BaseDeck, DIRECTION_COLUMN, Flex, JUSTIFY_CENTER, SPACING, } from '@opentrons/components' -import { useCreateDeckConfigurationMutation } from '@opentrons/react-api-client' -import { WASTE_CHUTE_LOAD_NAME } from '@opentrons/shared-data' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import { useUpdateDeckConfigurationMutation } from '@opentrons/react-api-client' import { ChildNavigation } from '../ChildNavigation' import { AddFixtureModal } from '../DeviceDetailsDeckConfiguration/AddFixtureModal' import { DeckConfigurationDiscardChangesModal } from '../DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal' import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import { Portal } from '../../App/portal' import type { - Cutout, + CutoutFixtureId, + CutoutId, DeckConfiguration, - Fixture, - FixtureLoadName, - LoadFixtureRunTimeCommand, } from '@opentrons/shared-data' import type { SetupScreens } from '../../pages/OnDeviceDisplay/ProtocolSetup' interface ProtocolSetupDeckConfigurationProps { - fixtureLocation: Cutout + cutoutId: CutoutId | null runId: string setSetupScreen: React.Dispatch> - providedFixtureOptions: FixtureLoadName[] + providedFixtureOptions: CutoutFixtureId[] } export function ProtocolSetupDeckConfiguration({ - fixtureLocation, + cutoutId, runId, setSetupScreen, providedFixtureOptions, @@ -51,46 +50,19 @@ export function ProtocolSetupDeckConfiguration({ ] = React.useState(false) const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - const STUBBED_LOAD_FIXTURE: LoadFixtureRunTimeCommand = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'D3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - } - - const requiredFixtureDetails = - mostRecentAnalysis?.commands != null - ? [ - // parseInitialLoadedFixturesByCutout(mostRecentAnalysis.commands), - STUBBED_LOAD_FIXTURE, - ] - : [] - const deckConfig = - (requiredFixtureDetails.map( - (fixture): Fixture | false => - fixture.params.fixtureId != null && { - fixtureId: fixture.params.fixtureId, - fixtureLocation: fixture.params.location.cutout, - loadName: fixture.params.loadName, - } - ) as DeckConfiguration) ?? [] + const simplestDeckConfig = getSimplestDeckConfigForProtocolCommands( + mostRecentAnalysis?.commands ?? [] + ).map(({ cutoutId, cutoutFixtureId }) => ({ cutoutId, cutoutFixtureId })) const [ currentDeckConfig, setCurrentDeckConfig, - ] = React.useState(deckConfig) + ] = React.useState(simplestDeckConfig) - const { createDeckConfiguration } = useCreateDeckConfigurationMutation() + const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() const handleClickConfirm = (): void => { - createDeckConfiguration(currentDeckConfig) + updateDeckConfiguration(currentDeckConfig) setSetupScreen('modules') } @@ -102,9 +74,9 @@ export function ProtocolSetupDeckConfiguration({ setShowConfirmationModal={setShowDiscardChangeModal} /> ) : null} - {showConfigurationModal && fixtureLocation != null ? ( + {showConfigurationModal && cutoutId != null ? ( - {/* DeckConfigurator will be replaced by BaseDeck when RAUT-793 is ready */} - {}} - handleClickRemove={() => {}} +
diff --git a/app/src/organisms/ProtocolSetupLabware/LabwareMapViewModal.tsx b/app/src/organisms/ProtocolSetupLabware/LabwareMapViewModal.tsx index b56629a6876..13bfb49a3b2 100644 --- a/app/src/organisms/ProtocolSetupLabware/LabwareMapViewModal.tsx +++ b/app/src/organisms/ProtocolSetupLabware/LabwareMapViewModal.tsx @@ -5,7 +5,7 @@ import { BaseDeck } from '@opentrons/components' import { FLEX_ROBOT_TYPE, THERMOCYCLER_MODULE_V1 } from '@opentrons/shared-data' import { Modal } from '../../molecules/Modal' -import { getDeckConfigFromProtocolCommands } from '../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import { getStandardDeckViewLayerBlockList } from '../Devices/ProtocolRun/utils/getStandardDeckViewLayerBlockList' import { getLabwareRenderInfo } from '../Devices/ProtocolRun/utils/getLabwareRenderInfo' import { AttachedProtocolModuleMatch } from '../ProtocolSetupModulesAndDeck/utils' @@ -42,7 +42,7 @@ export function LabwareMapViewModal( mostRecentAnalysis, } = props const { t } = useTranslation('protocol_setup') - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( mostRecentAnalysis?.commands ?? [] ) const labwareRenderInfo = diff --git a/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx b/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx index 5c9bf19d6ed..5b7d63a5277 100644 --- a/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx +++ b/app/src/organisms/ProtocolSetupLabware/__tests__/LabwareMapViewModal.test.tsx @@ -10,7 +10,7 @@ import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import deckDefFixture from '@opentrons/shared-data/deck/fixtures/3/deckExample.json' import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' import { i18n } from '../../../i18n' -import { getDeckConfigFromProtocolCommands } from '../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../resources/deck_configuration/utils' import { getLabwareRenderInfo } from '../../Devices/ProtocolRun/utils/getLabwareRenderInfo' import { getStandardDeckViewLayerBlockList } from '../../Devices/ProtocolRun/utils/getStandardDeckViewLayerBlockList' import { mockProtocolModuleInfo } from '../__fixtures__' @@ -32,8 +32,8 @@ jest.mock('../../../redux/config') const mockGetLabwareRenderInfo = getLabwareRenderInfo as jest.MockedFunction< typeof getLabwareRenderInfo > -const mockGetDeckConfigFromProtocolCommands = getDeckConfigFromProtocolCommands as jest.MockedFunction< - typeof getDeckConfigFromProtocolCommands +const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction< + typeof getSimplestDeckConfigForProtocolCommands > const mockBaseDeck = BaseDeck as jest.MockedFunction @@ -53,7 +53,7 @@ const render = (props: React.ComponentProps) => { describe('LabwareMapViewModal', () => { beforeEach(() => { mockGetLabwareRenderInfo.mockReturnValue({}) - mockGetDeckConfigFromProtocolCommands.mockReturnValue([]) + mockGetSimplestDeckConfigForProtocolCommands.mockReturnValue([]) }) afterEach(() => { diff --git a/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx b/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx index a9ee4c013a9..29d134884b9 100644 --- a/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx +++ b/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx @@ -7,8 +7,7 @@ import { useModulesQuery, } from '@opentrons/react-api-client' import { renderWithProviders } from '@opentrons/components' -import { getDeckDefFromRobotType } from '@opentrons/shared-data' -import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json' +import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot3_standard.json' import { i18n } from '../../../i18n' import { useMostRecentCompletedAnalysis } from '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' @@ -25,7 +24,6 @@ import { } from '../__fixtures__' jest.mock('@opentrons/react-api-client') -jest.mock('@opentrons/shared-data/js/helpers') jest.mock( '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' ) @@ -37,9 +35,6 @@ const mockUseCreateLiveCommandMutation = useCreateLiveCommandMutation as jest.Mo const mockUseModulesQuery = useModulesQuery as jest.MockedFunction< typeof useModulesQuery > -const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction< - typeof getDeckDefFromRobotType -> const mockUseMostRecentCompletedAnalysis = useMostRecentCompletedAnalysis as jest.MockedFunction< typeof useMostRecentCompletedAnalysis > @@ -72,9 +67,6 @@ describe('ProtocolSetupLabware', () => { when(mockUseMostRecentCompletedAnalysis) .calledWith(RUN_ID) .mockReturnValue(mockRecentAnalysis) - when(mockGetDeckDefFromRobotType) - .calledWith('OT-3 Standard') - .mockReturnValue(ot3StandardDeckDef as any) when(mockGetProtocolModulesInfo) .calledWith(mockRecentAnalysis, ot3StandardDeckDef as any) .mockReturnValue(mockProtocolModuleInfo) diff --git a/app/src/organisms/ProtocolSetupLabware/index.tsx b/app/src/organisms/ProtocolSetupLabware/index.tsx index 65da8173326..eace12eaada 100644 --- a/app/src/organisms/ProtocolSetupLabware/index.tsx +++ b/app/src/organisms/ProtocolSetupLabware/index.tsx @@ -142,6 +142,14 @@ export function ProtocolSetupLabware({ 'slotName' in selectedLabware?.location ) { location = + } else if ( + selectedLabware != null && + typeof selectedLabware.location === 'object' && + 'addressableAreaName' in selectedLabware?.location + ) { + location = ( + + ) } else if ( selectedLabware != null && typeof selectedLabware.location === 'object' && @@ -490,6 +498,9 @@ function RowLabware({ } else if ('slotName' in initialLocation) { slotName = initialLocation.slotName location = + } else if ('addressableAreaName' in initialLocation) { + slotName = initialLocation.addressableAreaName + location = } else if (matchedModuleType != null && matchedModule?.slotName != null) { slotName = matchedModule.slotName location = ( diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx index 7f5c0ba2542..910daa5db60 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx @@ -14,72 +14,68 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS, + getCutoutDisplayName, getFixtureDisplayName, - WASTE_CHUTE_LOAD_NAME, + SINGLE_SLOT_FIXTURES, } from '@opentrons/shared-data' -// import { parseInitialLoadedFixturesByCutout } from '@opentrons/api-client' -import { - CONFIGURED, - CONFLICTING, - NOT_CONFIGURED, - useLoadedFixturesConfigStatus, -} from '../../resources/deck_configuration/hooks' +import { useDeckConfigurationCompatibility } from '../../resources/deck_configuration/hooks' import { LocationConflictModal } from '../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' import { StyledText } from '../../atoms/text' import { Chip } from '../../atoms/Chip' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import type { CompletedProtocolAnalysis, - Cutout, - FixtureLoadName, - LoadFixtureRunTimeCommand, + CutoutFixtureId, + CutoutId, + RobotType, } from '@opentrons/shared-data' import type { SetupScreens } from '../../pages/OnDeviceDisplay/ProtocolSetup' interface FixtureTableProps { + robotType: RobotType mostRecentAnalysis: CompletedProtocolAnalysis | null setSetupScreen: React.Dispatch> - setFixtureLocation: (fixtureLocation: Cutout) => void - setProvidedFixtureOptions: (providedFixtureOptions: FixtureLoadName[]) => void + setCutoutId: (cutoutId: CutoutId) => void + setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void } export function FixtureTable({ + robotType, mostRecentAnalysis, setSetupScreen, - setFixtureLocation, + setCutoutId, setProvidedFixtureOptions, -}: FixtureTableProps): JSX.Element { +}: FixtureTableProps): JSX.Element | null { const { t, i18n } = useTranslation('protocol_setup') - const STUBBED_LOAD_FIXTURE: LoadFixtureRunTimeCommand = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'D3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', - } const [ showLocationConflictModal, setShowLocationConflictModal, ] = React.useState(false) - const requiredFixtureDetails = - mostRecentAnalysis?.commands != null - ? [ - // parseInitialLoadedFixturesByCutout(mostRecentAnalysis.commands), - STUBBED_LOAD_FIXTURE, - ] - : [] + const requiredFixtureDetails = getSimplestDeckConfigForProtocolCommands( + mostRecentAnalysis?.commands ?? [] + ) + const deckConfigCompatibility = useDeckConfigurationCompatibility( + robotType, + mostRecentAnalysis?.commands ?? [] + ) - const configurations = useLoadedFixturesConfigStatus(requiredFixtureDetails) + const nonSingleSlotDeckConfigCompatibility = deckConfigCompatibility.filter( + ({ requiredAddressableAreas }) => + // required AA list includes a non-single-slot AA + !requiredAddressableAreas.every(aa => + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS.includes(aa) + ) + ) + // fixture includes at least 1 required AA + const requiredDeckConfigCompatibility = nonSingleSlotDeckConfigCompatibility.filter( + fixture => fixture.requiredAddressableAreas.length > 0 + ) - return ( + return requiredDeckConfigCompatibility.length > 0 ? ( {t('location')} {t('status')} - {requiredFixtureDetails.map((fixture, index) => { - const configurationStatus = configurations.find( - configuration => configuration.id === fixture.id - )?.configurationStatus - - const statusNotReady = - configurationStatus === CONFLICTING || - configurationStatus === NOT_CONFIGURED + {requiredDeckConfigCompatibility.map( + ({ cutoutId, cutoutFixtureId, compatibleCutoutFixtureIds }, index) => { + const isCurrentFixtureCompatible = + cutoutFixtureId != null && + compatibleCutoutFixtureIds.includes(cutoutFixtureId) - let chipLabel: JSX.Element - let handleClick - if (statusNotReady) { - chipLabel = ( - <> - - - - ) - handleClick = - configurationStatus === CONFLICTING + let chipLabel: JSX.Element + let handleClick + if (!isCurrentFixtureCompatible) { + const isConflictingFixtureConfigured = + cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + chipLabel = ( + <> + + + + ) + handleClick = isConflictingFixtureConfigured ? () => setShowLocationConflictModal(true) : () => { - setFixtureLocation(fixture.params.location.cutout) - setProvidedFixtureOptions([fixture.params.loadName]) + setCutoutId(cutoutId) + setProvidedFixtureOptions(compatibleCutoutFixtureIds) setSetupScreen('deck configuration') } - } else if (configurationStatus === CONFIGURED) { - chipLabel = ( - - ) - // TODO(jr, 10/17/23): wire this up - // handleClick = () => setShowNotConfiguredModal(true) - - // shouldn't run into this case - } else { - chipLabel =
status label unknown
- } - - return ( - - {showLocationConflictModal ? ( - setShowLocationConflictModal(false)} - cutout={fixture.params.location.cutout} - requiredFixture={fixture.params.loadName} - isOnDevice={true} + } else { + chipLabel = ( + - ) : null} - - - - {getFixtureDisplayName(fixture.params.loadName)} - - - - - + ) + } + return ( + + {showLocationConflictModal ? ( + setShowLocationConflictModal(false)} + cutoutId={cutoutId} + requiredFixtureId={compatibleCutoutFixtureIds[0]} + isOnDevice={true} + /> + ) : null} - {chipLabel} + + + {cutoutFixtureId != null && isCurrentFixtureCompatible + ? getFixtureDisplayName(cutoutFixtureId) + : getFixtureDisplayName(compatibleCutoutFixtureIds?.[0])} + + + + + + + {chipLabel} + - - - ) - })} + + ) + } + )}
- ) + ) : null } diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/ModulesAndDeckMapViewModal.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/ModulesAndDeckMapViewModal.tsx index 5045795d771..d432f9d8bba 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/ModulesAndDeckMapViewModal.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/ModulesAndDeckMapViewModal.tsx @@ -6,7 +6,7 @@ import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { Modal } from '../../molecules/Modal' import { ModuleInfo } from '../Devices/ModuleInfo' -import { getDeckConfigFromProtocolCommands } from '../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../resources/deck_configuration/utils' import { getStandardDeckViewLayerBlockList } from '../Devices/ProtocolRun/utils/getStandardDeckViewLayerBlockList' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' @@ -36,7 +36,7 @@ export function ModulesAndDeckMapViewModal({ if (protocolAnalysis == null) return null - const deckConfig = getDeckConfigFromProtocolCommands( + const deckConfig = getSimplestDeckConfigForProtocolCommands( protocolAnalysis.commands ) diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx index ff8a3da51ee..c24bbbe2537 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/FixtureTable.test.tsx @@ -1,55 +1,27 @@ import * as React from 'react' import { renderWithProviders } from '@opentrons/components' import { - STAGING_AREA_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + FLEX_ROBOT_TYPE, + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, } from '@opentrons/shared-data' import { i18n } from '../../../i18n' -import { useLoadedFixturesConfigStatus } from '../../../resources/deck_configuration/hooks' +import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks' import { LocationConflictModal } from '../../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal' import { FixtureTable } from '../FixtureTable' -import type { LoadFixtureRunTimeCommand } from '@opentrons/shared-data' jest.mock('../../../resources/deck_configuration/hooks') jest.mock('../../Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal') -const mockUseLoadedFixturesConfigStatus = useLoadedFixturesConfigStatus as jest.MockedFunction< - typeof useLoadedFixturesConfigStatus -> const mockLocationConflictModal = LocationConflictModal as jest.MockedFunction< typeof LocationConflictModal > -const mockLoadedFixture = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'D3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', -} as LoadFixtureRunTimeCommand - -const mockLoadedStagingAreaFixture = { - id: 'stubbed_load_fixture_2', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: STAGING_AREA_LOAD_NAME, - location: { cutout: 'D3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', -} as LoadFixtureRunTimeCommand +const mockUseDeckConfigurationCompatibility = useDeckConfigurationCompatibility as jest.MockedFunction< + typeof useDeckConfigurationCompatibility +> const mockSetSetupScreen = jest.fn() -const mockSetFixtureLocation = jest.fn() +const mockSetCutoutId = jest.fn() const mockSetProvidedFixtureOptions = jest.fn() const render = (props: React.ComponentProps) => { @@ -63,16 +35,24 @@ describe('FixtureTable', () => { beforeEach(() => { props = { mostRecentAnalysis: [] as any, + robotType: FLEX_ROBOT_TYPE, setSetupScreen: mockSetSetupScreen, - setFixtureLocation: mockSetFixtureLocation, + setCutoutId: mockSetCutoutId, setProvidedFixtureOptions: mockSetProvidedFixtureOptions, } - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { ...mockLoadedFixture, configurationStatus: 'configured' }, - ]) mockLocationConflictModal.mockReturnValue(
mock location conflict modal
) + mockUseDeckConfigurationCompatibility.mockReturnValue([ + { + cutoutId: 'cutoutD3', + cutoutFixtureId: STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + requiredAddressableAreas: ['D4'], + compatibleCutoutFixtureIds: [ + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + ], + }, + ]) }) it('should render table header and contents', () => { @@ -84,46 +64,39 @@ describe('FixtureTable', () => { it('should render the current status - configured', () => { props = { ...props, - mostRecentAnalysis: { commands: [mockLoadedFixture] } as any, + // TODO(bh, 2023-11-13): mock load labware etc commands + mostRecentAnalysis: { commands: [] } as any, } const [{ getByText }] = render(props) getByText('Configured') }) - it('should render the current status - not configured', () => { - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { ...mockLoadedFixture, configurationStatus: 'not configured' }, - ]) - props = { - ...props, - mostRecentAnalysis: { commands: [mockLoadedStagingAreaFixture] } as any, - } - const [{ getByText }] = render(props) - getByText('Not configured') - }) - it('should render the current status - conflicting', () => { - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { ...mockLoadedFixture, configurationStatus: 'conflicting' }, - ]) - props = { - ...props, - mostRecentAnalysis: { commands: [mockLoadedStagingAreaFixture] } as any, - } - const [{ getByText, getAllByText }] = render(props) - getByText('Location conflict').click() - getAllByText('mock location conflict modal') - }) - it('should call a mock function when tapping not configured row', () => { - mockUseLoadedFixturesConfigStatus.mockReturnValue([ - { ...mockLoadedFixture, configurationStatus: 'not configured' }, - ]) - props = { - ...props, - mostRecentAnalysis: { commands: [mockLoadedStagingAreaFixture] } as any, - } - const [{ getByText }] = render(props) - getByText('Not configured').click() - expect(mockSetFixtureLocation).toHaveBeenCalledWith('D3') - expect(mockSetSetupScreen).toHaveBeenCalledWith('deck configuration') - expect(mockSetProvidedFixtureOptions).toHaveBeenCalledWith(['wasteChute']) - }) + // TODO(bh, 2023-11-14): implement test cases when example JSON protocol fixtures exist + // it('should render the current status - not configured', () => { + // props = { + // ...props, + // mostRecentAnalysis: { commands: [] } as any, + // } + // const [{ getByText }] = render(props) + // getByText('Not configured') + // }) + // it('should render the current status - conflicting', () => { + // props = { + // ...props, + // mostRecentAnalysis: { commands: [] } as any, + // } + // const [{ getByText, getAllByText }] = render(props) + // getByText('Location conflict').click() + // getAllByText('mock location conflict modal') + // }) + // it('should call a mock function when tapping not configured row', () => { + // props = { + // ...props, + // mostRecentAnalysis: { commands: [] } as any, + // } + // const [{ getByText }] = render(props) + // getByText('Not configured').click() + // expect(mockSetCutoutId).toHaveBeenCalledWith('cutoutD3') + // expect(mockSetSetupScreen).toHaveBeenCalledWith('deck configuration') + // expect(mockSetProvidedFixtureOptions).toHaveBeenCalledWith(['wasteChute']) + // }) }) diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapViewModal.test.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapViewModal.test.tsx index 1882f9947cc..343a692a4f4 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapViewModal.test.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ModulesAndDeckMapViewModal.test.tsx @@ -1,14 +1,10 @@ import * as React from 'react' import { when, resetAllWhenMocks } from 'jest-when' -import { - renderWithProviders, - BaseDeck, - EXTENDED_DECK_CONFIG_FIXTURE, -} from '@opentrons/components' +import { renderWithProviders, BaseDeck } from '@opentrons/components' import { i18n } from '../../../i18n' -import { getDeckConfigFromProtocolCommands } from '../../../resources/deck_configuration/utils' +import { getSimplestDeckConfigForProtocolCommands } from '../../../resources/deck_configuration/utils' import { ModulesAndDeckMapViewModal } from '../ModulesAndDeckMapViewModal' jest.mock('@opentrons/components/src/hardware-sim/BaseDeck') @@ -92,8 +88,8 @@ const render = ( } const mockBaseDeck = BaseDeck as jest.MockedFunction -const mockGetDeckConfigFromProtocolCommands = getDeckConfigFromProtocolCommands as jest.MockedFunction< - typeof getDeckConfigFromProtocolCommands +const mockGetSimplestDeckConfigForProtocolCommands = getSimplestDeckConfigForProtocolCommands as jest.MockedFunction< + typeof getSimplestDeckConfigForProtocolCommands > describe('ModulesAndDeckMapViewModal', () => { @@ -106,8 +102,9 @@ describe('ModulesAndDeckMapViewModal', () => { runId: mockRunId, protocolAnalysis: PROTOCOL_ANALYSIS, } - when(mockGetDeckConfigFromProtocolCommands).mockReturnValue( - EXTENDED_DECK_CONFIG_FIXTURE + when(mockGetSimplestDeckConfigForProtocolCommands).mockReturnValue( + // TODO(bh, 2023-11-13): mock cutout config protocol spec + [] ) mockBaseDeck.mockReturnValue(
mock BaseDeck
) }) diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx index d5dd9c2d787..422b3df24dd 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx @@ -7,12 +7,10 @@ import { MemoryRouter } from 'react-router-dom' import { renderWithProviders } from '@opentrons/components' import { useDeckConfigurationQuery } from '@opentrons/react-api-client' import { - DeckConfiguration, - Fixture, getDeckDefFromRobotType, - STAGING_AREA_LOAD_NAME, + WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, } from '@opentrons/shared-data' -import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json' +import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot3_standard.json' import { i18n } from '../../../i18n' import { useChainLiveCommands } from '../../../resources/runs/hooks' @@ -38,6 +36,8 @@ import { FixtureTable } from '../FixtureTable' import { ModulesAndDeckMapViewModal } from '../ModulesAndDeckMapViewModal' import { ProtocolSetupModulesAndDeck } from '..' +import type { CutoutConfig, DeckConfiguration } from '@opentrons/shared-data' + jest.mock('@opentrons/react-api-client') jest.mock('../../../resources/runs/hooks') jest.mock('@opentrons/shared-data/js/helpers') @@ -103,7 +103,7 @@ const mockModulesAndDeckMapViewModal = ModulesAndDeckMapViewModal as jest.Mocked const ROBOT_NAME = 'otie' const RUN_ID = '1' const mockSetSetupScreen = jest.fn() -const mockSetFixtureLocation = jest.fn() +const mockSetCutoutId = jest.fn() const mockSetProvidedFixtureOptions = jest.fn() const calibratedMockApiHeaterShaker = { @@ -118,11 +118,10 @@ const calibratedMockApiHeaterShaker = { last_modified: '2023-06-01T14:42:20.131798+00:00', }, } -const mockFixture = { - fixtureId: 'mockId', - fixtureLocation: '10' as any, - loadName: STAGING_AREA_LOAD_NAME, -} as Fixture +const mockFixture: CutoutConfig = { + cutoutId: 'cutoutD3', + cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, +} const render = () => { return renderWithProviders( @@ -130,7 +129,7 @@ const render = () => { , @@ -196,6 +195,12 @@ describe('ProtocolSetupModulesAndDeck', () => { }) it('should render text and buttons', () => { + mockGetAttachedProtocolModuleMatches.mockReturnValue([ + { + ...mockProtocolModuleInfo[0], + attachedModuleMatch: calibratedMockApiHeaterShaker, + }, + ]) const [{ getByRole, getByText }] = render() getByText('Module') getByText('Location') @@ -363,6 +368,7 @@ describe('ProtocolSetupModulesAndDeck', () => { { ...mockProtocolModuleInfo[0], attachedModuleMatch: calibratedMockApiHeaterShaker, + slotName: 'D3', }, ]) const [{ getByText }] = render() diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx index 5a58f031275..ab6c3fd5cd4 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx @@ -16,11 +16,12 @@ import { } from '@opentrons/components' import { useDeckConfigurationQuery } from '@opentrons/react-api-client' import { + FLEX_ROBOT_TYPE, getDeckDefFromRobotType, getModuleDisplayName, getModuleType, NON_CONNECTING_MODULE_TYPES, - STANDARD_SLOT_LOAD_NAME, + SINGLE_SLOT_FIXTURES, TC_MODULE_LOCATION_OT3, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' @@ -38,7 +39,8 @@ import { import { MultipleModulesModal } from '../Devices/ProtocolRun/SetupModuleAndDeck/MultipleModulesModal' import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { useMostRecentCompletedAnalysis } from '../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { ROBOT_MODEL_OT3, getLocalRobot } from '../../redux/discovery' +import { getLocalRobot } from '../../redux/discovery' +import { getCutoutIdForSlotName } from '../../resources/deck_configuration/utils' import { useChainLiveCommands } from '../../resources/runs/hooks' import { getModulePrepCommands, @@ -57,7 +59,11 @@ import { FixtureTable } from './FixtureTable' import { ModulesAndDeckMapViewModal } from './ModulesAndDeckMapViewModal' import type { CommandData } from '@opentrons/api-client' -import type { Cutout, Fixture, FixtureLoadName } from '@opentrons/shared-data' +import type { + CutoutConfig, + CutoutId, + CutoutFixtureId, +} from '@opentrons/shared-data' import type { SetupScreens } from '../../pages/OnDeviceDisplay/ProtocolSetup' import type { ProtocolCalibrationStatus } from '../../organisms/Devices/hooks' import type { AttachedProtocolModuleMatch } from './utils' @@ -75,7 +81,7 @@ interface RenderModuleStatusProps { commands: ModulePrepCommandsType[], continuePastCommandFailure: boolean ) => Promise - conflictedFixture?: Fixture + conflictedFixture?: CutoutConfig } function RenderModuleStatus({ @@ -186,7 +192,7 @@ interface RowModuleProps { ) => Promise prepCommandErrorMessage: string setPrepCommandErrorMessage: React.Dispatch> - conflictedFixture?: Fixture + conflictedFixture?: CutoutConfig } function RowModule({ @@ -229,7 +235,7 @@ function RowModule({ {showLocationConflictModal && conflictedFixture != null ? ( setShowLocationConflictModal(false)} - cutout={conflictedFixture.fixtureLocation} + cutoutId={conflictedFixture.cutoutId} requiredModule={module.moduleDef.model} isOnDevice={true} /> @@ -302,8 +308,8 @@ function RowModule({ interface ProtocolSetupModulesAndDeckProps { runId: string setSetupScreen: React.Dispatch> - setFixtureLocation: (fixtureLocation: Cutout) => void - setProvidedFixtureOptions: (providedFixtureOptions: FixtureLoadName[]) => void + setCutoutId: (cutoutId: CutoutId) => void + setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void } /** @@ -312,7 +318,7 @@ interface ProtocolSetupModulesAndDeckProps { export function ProtocolSetupModulesAndDeck({ runId, setSetupScreen, - setFixtureLocation, + setCutoutId, setProvidedFixtureOptions, }: ProtocolSetupModulesAndDeckProps): JSX.Element { const { i18n, t } = useTranslation('protocol_setup') @@ -337,7 +343,7 @@ export function ProtocolSetupModulesAndDeck({ const { data: deckConfig } = useDeckConfigurationQuery() const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - const deckDef = getDeckDefFromRobotType(ROBOT_MODEL_OT3) + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) const attachedModules = useAttachedModules({ @@ -358,6 +364,8 @@ export function ProtocolSetupModulesAndDeck({ protocolModulesInfo ) + const hasModules = attachedProtocolModuleMatches.length > 0 + const { missingModuleIds, remainingAttachedModules, @@ -401,6 +409,7 @@ export function ProtocolSetupModulesAndDeck({ flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing24} marginTop="7.75rem" + marginBottom={SPACING.spacing80} > {isModuleMismatch && !clearModuleMismatchBanner ? ( ) : null} - - - {t('module')} - {t('location')} - {t('status')} - - {attachedProtocolModuleMatches.map(module => { - // check for duplicate module model in list of modules for protocol - const isDuplicateModuleModel = protocolModulesInfo - // filter out current module - .filter(otherModule => otherModule.moduleId !== module.moduleId) - // check for existence of another module of same model - .some( - otherModule => - otherModule.moduleDef.model === module.moduleDef.model + {hasModules ? ( + + + {t('module')} + {t('location')} + {t('status')} + + {attachedProtocolModuleMatches.map(module => { + // check for duplicate module model in list of modules for protocol + const isDuplicateModuleModel = protocolModulesInfo + // filter out current module + .filter( + otherModule => otherModule.moduleId !== module.moduleId + ) + // check for existence of another module of same model + .some( + otherModule => + otherModule.moduleDef.model === module.moduleDef.model + ) + + const cutoutIdForSlotName = getCutoutIdForSlotName( + module.slotName, + deckDef ) - return ( - - fixture.fixtureLocation === module.slotName && - fixture.loadName !== STANDARD_SLOT_LOAD_NAME - )} - /> - ) - })} - + + return ( + + fixture.cutoutId === cutoutIdForSlotName && + fixture.cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) + )} + /> + ) + })} + + ) : null} diff --git a/app/src/organisms/ProtocolsLanding/ProtocolList.tsx b/app/src/organisms/ProtocolsLanding/ProtocolList.tsx index d12e6c8fd3a..d812fed10f7 100644 --- a/app/src/organisms/ProtocolsLanding/ProtocolList.tsx +++ b/app/src/organisms/ProtocolsLanding/ProtocolList.tsx @@ -29,7 +29,7 @@ import { StyledText } from '../../atoms/text' import { Slideout } from '../../atoms/Slideout' import { ChooseRobotToRunProtocolSlideout } from '../ChooseRobotToRunProtocolSlideout' import { SendProtocolToOT3Slideout } from '../SendProtocolToOT3Slideout' -import { UploadInput } from './UploadInput' +import { ProtocolUploadInput } from './ProtocolUploadInput' import { ProtocolCard } from './ProtocolCard' import { EmptyStateLinks } from './EmptyStateLinks' import { MenuItem } from '../../atoms/MenuList/MenuItem' @@ -254,7 +254,9 @@ export function ProtocolList(props: ProtocolListProps): JSX.Element | null { onCloseClick={() => setShowImportProtocolSlideout(false)} > - setShowImportProtocolSlideout(false)} /> + setShowImportProtocolSlideout(false)} + />
diff --git a/app/src/organisms/ProtocolsLanding/UploadInput.tsx b/app/src/organisms/ProtocolsLanding/ProtocolUploadInput.tsx similarity index 83% rename from app/src/organisms/ProtocolsLanding/UploadInput.tsx rename to app/src/organisms/ProtocolsLanding/ProtocolUploadInput.tsx index aa454b9d690..181da16300f 100644 --- a/app/src/organisms/ProtocolsLanding/UploadInput.tsx +++ b/app/src/organisms/ProtocolsLanding/ProtocolUploadInput.tsx @@ -10,7 +10,7 @@ import { SPACING, } from '@opentrons/components' import { StyledText } from '../../atoms/text' -import { UploadInput as FileImporter } from '../../molecules/UploadInput' +import { UploadInput } from '../../molecules/UploadInput' import { addProtocol } from '../../redux/protocol-storage' import { useTrackEvent, @@ -24,8 +24,9 @@ export interface UploadInputProps { onUpload?: () => void } -// TODO(bc, 2022-3-21): consider making this generic for any file upload and adding it to molecules/organisms with onUpload taking the files from the event -export function UploadInput(props: UploadInputProps): JSX.Element | null { +export function ProtocolUploadInput( + props: UploadInputProps +): JSX.Element | null { const { t } = useTranslation(['protocol_info', 'shared']) const dispatch = useDispatch() const logger = useLogger(__filename) @@ -49,7 +50,7 @@ export function UploadInput(props: UploadInputProps): JSX.Element | null { alignItems={ALIGN_CENTER} marginY={SPACING.spacing20} > - handleUpload(file)} uploadText={t('valid_file_types')} dragAndDropText={ diff --git a/app/src/organisms/ProtocolsLanding/ProtocolsEmptyState.tsx b/app/src/organisms/ProtocolsLanding/ProtocolsEmptyState.tsx index 8388a075d5f..a6238653e6e 100644 --- a/app/src/organisms/ProtocolsLanding/ProtocolsEmptyState.tsx +++ b/app/src/organisms/ProtocolsLanding/ProtocolsEmptyState.tsx @@ -9,7 +9,7 @@ import { } from '@opentrons/components' import { StyledText } from '../../atoms/text' -import { UploadInput } from './UploadInput' +import { ProtocolUploadInput } from './ProtocolUploadInput' import { EmptyStateLinks } from './EmptyStateLinks' export function ProtocolsEmptyState(): JSX.Element | null { const { t } = useTranslation('protocol_info') @@ -25,7 +25,7 @@ export function ProtocolsEmptyState(): JSX.Element | null { {t('import_a_file')} - +
) diff --git a/app/src/organisms/ProtocolsLanding/__tests__/UploadInput.test.tsx b/app/src/organisms/ProtocolsLanding/__tests__/UploadInput.test.tsx index 83455f3b071..c269696c5ce 100644 --- a/app/src/organisms/ProtocolsLanding/__tests__/UploadInput.test.tsx +++ b/app/src/organisms/ProtocolsLanding/__tests__/UploadInput.test.tsx @@ -8,13 +8,13 @@ import { useTrackEvent, ANALYTICS_IMPORT_PROTOCOL_TO_APP, } from '../../../redux/analytics' -import { UploadInput } from '../UploadInput' +import { ProtocolUploadInput } from '../ProtocolUploadInput' jest.mock('../../../redux/analytics') const mockUseTrackEvent = useTrackEvent as jest.Mock -describe('UploadInput', () => { +describe('ProtocolUploadInput', () => { let onUpload: jest.MockedFunction<() => {}> let trackEvent: jest.MockedFunction let render: () => ReturnType[0] @@ -26,7 +26,7 @@ describe('UploadInput', () => { render = () => { return renderWithProviders( - + , { i18nInstance: i18n, @@ -41,7 +41,7 @@ describe('UploadInput', () => { it('renders correct contents for empty state', () => { const { findByText, getByRole } = render() - getByRole('button', { name: 'Choose File' }) + getByRole('button', { name: 'Upload' }) findByText('Drag and drop or') findByText('your files') findByText( @@ -52,7 +52,7 @@ describe('UploadInput', () => { it('opens file select on button click', () => { const { getByRole, getByTestId } = render() - const button = getByRole('button', { name: 'Choose File' }) + const button = getByRole('button', { name: 'Upload' }) const input = getByTestId('file_input') input.click = jest.fn() fireEvent.click(button) diff --git a/app/src/organisms/RobotSettingsCalibration/CalibrationDataDownload.tsx b/app/src/organisms/RobotSettingsCalibration/CalibrationDataDownload.tsx index 4755352fdee..4a261b331d9 100644 --- a/app/src/organisms/RobotSettingsCalibration/CalibrationDataDownload.tsx +++ b/app/src/organisms/RobotSettingsCalibration/CalibrationDataDownload.tsx @@ -91,7 +91,11 @@ export function CalibrationDataDownload({ } return ( - + {isFlex diff --git a/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx b/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx index fe8667f791f..1095694392f 100644 --- a/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx +++ b/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx @@ -52,8 +52,6 @@ interface DeviceResetProps { setCurrentOption: SetSettingOption } -// ToDo (kk:08/30/2023) lines that are related to module calibration will be activated when the be is ready. -// The tests for that will be added. export function DeviceReset({ robotName, setCurrentOption, @@ -86,20 +84,19 @@ export function DeviceReset({ ({ id }) => !['authorizedKeys'].includes(id) ) + const isEveryOptionSelected = (obj: ResetConfigRequest): boolean => { + for (const key of targetOptionsOrder) { + if (obj != null && !obj[key]) { + return false + } + } + return true + } + const handleClick = (): void => { if (resetOptions != null) { - // remove clearAllStoredData since its not a setting on the backend - const { clearAllStoredData, ...serverResetOptions } = resetOptions - if (Boolean(clearAllStoredData)) { - dispatchRequest( - resetConfig(robotName, { - ...serverResetOptions, - onDeviceDisplay: true, - }) - ) - } else { - dispatchRequest(resetConfig(robotName, serverResetOptions)) - } + const { ...serverResetOptions } = resetOptions + dispatchRequest(resetConfig(robotName, serverResetOptions)) } } @@ -145,6 +142,33 @@ export function DeviceReset({ dispatch(fetchResetConfigOptions(robotName)) }, [dispatch, robotName]) + React.useEffect(() => { + if ( + isEveryOptionSelected(resetOptions) && + (!resetOptions.authorizedKeys || !resetOptions.onDeviceDisplay) + ) { + setResetOptions({ + ...resetOptions, + authorizedKeys: true, + onDeviceDisplay: true, + }) + } + }, [resetOptions]) + + React.useEffect(() => { + if ( + !isEveryOptionSelected(resetOptions) && + resetOptions.authorizedKeys && + resetOptions.onDeviceDisplay + ) { + setResetOptions({ + ...resetOptions, + authorizedKeys: false, + onDeviceDisplay: false, + }) + } + }, [resetOptions]) + return ( {showConfirmationModal && ( @@ -218,7 +242,8 @@ export function DeviceReset({ value="clearAllStoredData" onChange={() => { setResetOptions( - Boolean(resetOptions.clearAllStoredData) + (resetOptions.authorizedKeys ?? false) && + (resetOptions.onDeviceDisplay ?? false) ? {} : availableOptions.reduce( (acc, val) => { @@ -227,14 +252,18 @@ export function DeviceReset({ [val.id]: true, } }, - { clearAllStoredData: true } + { authorizedKeys: true, onDeviceDisplay: true } ) ) }} /> @@ -243,7 +272,9 @@ export function DeviceReset({ { getByText('Clear module calibration') getByText('Clear protocol run history') getByText('Clears information about past runs of all protocols.') + getByText('Clear all stored data') + getByText( + 'Resets all settings. You’ll have to redo initial setup before using the robot again.' + ) expect(queryByText('Clear the ssh authorized keys')).not.toBeInTheDocument() expect(getByTestId('DeviceReset_clear_data_button')).toBeDisabled() }) @@ -115,4 +119,71 @@ describe('DeviceReset', () => { mockResetConfig('mockRobot', clearMockResetOptions) ) }) + + it('when tapping clear all stored data, all options are active', () => { + const clearMockResetOptions = { + pipetteOffsetCalibrations: true, + moduleCalibration: true, + runsHistory: true, + gripperOffsetCalibrations: true, + authorizedKeys: true, + onDeviceDisplay: true, + } + + const [{ getByText }] = render(props) + getByText('Clear all stored data').click() + const clearButton = getByText('Clear data and restart robot') + fireEvent.click(clearButton) + getByText('Are you sure you want to reset your device?') + fireEvent.click(getByText('Confirm')) + expect(dispatchApiRequest).toBeCalledWith( + mockResetConfig('mockRobot', clearMockResetOptions) + ) + }) + + it('when tapping all options except clear all stored data, all options are active', () => { + const clearMockResetOptions = { + pipetteOffsetCalibrations: true, + moduleCalibration: true, + runsHistory: true, + gripperOffsetCalibrations: true, + authorizedKeys: true, + onDeviceDisplay: true, + } + + const [{ getByText }] = render(props) + getByText('Clear pipette calibration').click() + getByText('Clear gripper calibration').click() + getByText('Clear module calibration').click() + getByText('Clear protocol run history').click() + const clearButton = getByText('Clear data and restart robot') + fireEvent.click(clearButton) + getByText('Are you sure you want to reset your device?') + fireEvent.click(getByText('Confirm')) + expect(dispatchApiRequest).toBeCalledWith( + mockResetConfig('mockRobot', clearMockResetOptions) + ) + }) + + it('when tapping clear all stored data and unselect one options, all options are not active', () => { + const clearMockResetOptions = { + pipetteOffsetCalibrations: false, + moduleCalibration: true, + runsHistory: true, + gripperOffsetCalibrations: true, + authorizedKeys: false, + onDeviceDisplay: false, + } + + const [{ getByText }] = render(props) + getByText('Clear all stored data').click() + getByText('Clear pipette calibration').click() + const clearButton = getByText('Clear data and restart robot') + fireEvent.click(clearButton) + getByText('Are you sure you want to reset your device?') + fireEvent.click(getByText('Confirm')) + expect(dispatchApiRequest).toBeCalledWith( + mockResetConfig('mockRobot', clearMockResetOptions) + ) + }) }) diff --git a/app/src/organisms/UpdateAppModal/__tests__/UpdateAppModal.test.tsx b/app/src/organisms/UpdateAppModal/__tests__/UpdateAppModal.test.tsx index a7d8069cf1e..939755c8208 100644 --- a/app/src/organisms/UpdateAppModal/__tests__/UpdateAppModal.test.tsx +++ b/app/src/organisms/UpdateAppModal/__tests__/UpdateAppModal.test.tsx @@ -1,11 +1,13 @@ import * as React from 'react' import { when } from 'jest-when' import { fireEvent } from '@testing-library/react' + import { renderWithProviders } from '@opentrons/components' + import { i18n } from '../../../i18n' import * as Shell from '../../../redux/shell' -import { UpdateAppModal, UpdateAppModalProps } from '..' import { useRemoveActiveAppUpdateToast } from '../../Alerts' +import { UpdateAppModal, UpdateAppModalProps, RELEASE_NOTES_URL_BASE } from '..' import type { State } from '../../../redux/types' import type { ShellUpdateState } from '../../../redux/shell/types' @@ -33,6 +35,9 @@ const mockUseRemoveActiveAppUpdateToast = useRemoveActiveAppUpdateToast as jest. const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, + initialState: { + shell: { update: { info: { version: '7.0.0' }, available: true } }, + }, }) } @@ -76,6 +81,14 @@ describe('UpdateAppModal', () => { fireEvent.click(getByText('Remind me later')) expect(closeModal).toHaveBeenCalled() }) + + it('renders a release notes link pointing to the Github releases page', () => { + const [{ getByText }] = render(props) + + const link = getByText('Release notes') + expect(link).toHaveAttribute('href', RELEASE_NOTES_URL_BASE + '7.0.0') + }) + it('shows error modal on error', () => { getShellUpdateState.mockReturnValue({ error: { diff --git a/app/src/organisms/UpdateAppModal/index.tsx b/app/src/organisms/UpdateAppModal/index.tsx index 16f0fc7e517..367a6edc779 100644 --- a/app/src/organisms/UpdateAppModal/index.tsx +++ b/app/src/organisms/UpdateAppModal/index.tsx @@ -8,20 +8,23 @@ import { ALIGN_CENTER, COLORS, DIRECTION_COLUMN, - JUSTIFY_FLEX_END, SPACING, Flex, NewPrimaryBtn, NewSecondaryBtn, BORDERS, + JUSTIFY_SPACE_BETWEEN, + JUSTIFY_SPACE_AROUND, } from '@opentrons/components' import { getShellUpdateState, + getAvailableShellUpdate, downloadShellUpdate, applyShellUpdate, } from '../../redux/shell' +import { ExternalLink } from '../../atoms/Link/ExternalLink' import { ReleaseNotes } from '../../molecules/ReleaseNotes' import { LegacyModal } from '../../molecules/LegacyModal' import { Banner } from '../../atoms/Banner' @@ -56,7 +59,8 @@ const PlaceholderError = ({ ) } - +export const RELEASE_NOTES_URL_BASE = + 'https://github.com/Opentrons/opentrons/releases/tag/v' const UPDATE_ERROR = 'Update Error' const FOOTER_BUTTON_STYLE = css` text-transform: lowercase; @@ -105,6 +109,7 @@ export function UpdateAppModal(props: UpdateAppModalProps): JSX.Element { const { t } = useTranslation('app_settings') const history = useHistory() const { removeActiveAppUpdateToast } = useRemoveActiveAppUpdateToast() + const availableAppUpdateVersion = useSelector(getAvailableShellUpdate) ?? '' if (downloaded) setTimeout(() => dispatch(applyShellUpdate()), RESTART_APP_AFTER_TIME) @@ -117,21 +122,33 @@ export function UpdateAppModal(props: UpdateAppModalProps): JSX.Element { removeActiveAppUpdateToast() const appUpdateFooter = ( - - - {t('remind_later')} - - dispatch(downloadShellUpdate())} - marginRight={SPACING.spacing12} - css={FOOTER_BUTTON_STYLE} + + - {t('update_app_now')} - + {t('release_notes')} + + + + {t('remind_later')} + + dispatch(downloadShellUpdate())} + marginRight={SPACING.spacing12} + css={FOOTER_BUTTON_STYLE} + > + {t('update_app_now')} + + ) diff --git a/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx b/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx index ebaccbd62a4..7f24c1e91fc 100644 --- a/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx +++ b/app/src/pages/DeckConfiguration/__tests__/DeckConfiguration.test.tsx @@ -5,9 +5,9 @@ import { when, resetAllWhenMocks } from 'jest-when' import { DeckConfigurator, renderWithProviders } from '@opentrons/components' import { useDeckConfigurationQuery, - useCreateDeckConfigurationMutation, + useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' -import { TRASH_BIN_LOAD_NAME } from '@opentrons/shared-data' +import { TRASH_BIN_ADAPTER_FIXTURE } from '@opentrons/shared-data' import { i18n } from '../../../i18n' import { DeckFixtureSetupInstructionsModal } from '../../../organisms/DeviceDetailsDeckConfiguration/DeckFixtureSetupInstructionsModal' @@ -17,7 +17,7 @@ import { DeckConfigurationEditor } from '..' import type { UseQueryResult } from 'react-query' import type { DeckConfiguration } from '@opentrons/shared-data' -const mockCreateDeckConfiguration = jest.fn() +const mockUpdateDeckConfiguration = jest.fn() const mockGoBack = jest.fn() jest.mock('react-router-dom', () => { const reactRouterDom = jest.requireActual('react-router-dom') @@ -29,9 +29,8 @@ jest.mock('react-router-dom', () => { const mockDeckConfig = [ { - fixtureId: 'mockFixtureIdC3', - fixtureLocation: 'C3', - loadName: TRASH_BIN_LOAD_NAME, + cutoutId: 'cutoutC3', + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, }, ] @@ -56,8 +55,8 @@ const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFu const mockDeckConfigurationDiscardChangesModal = DeckConfigurationDiscardChangesModal as jest.MockedFunction< typeof DeckConfigurationDiscardChangesModal > -const mockUseCreateDeckConfigurationMutation = useCreateDeckConfigurationMutation as jest.MockedFunction< - typeof useCreateDeckConfigurationMutation +const mockUseUpdateDeckConfigurationMutation = useUpdateDeckConfigurationMutation as jest.MockedFunction< + typeof useUpdateDeckConfigurationMutation > const render = () => { @@ -83,8 +82,8 @@ describe('DeckConfigurationEditor', () => { mockDeckConfigurationDiscardChangesModal.mockReturnValue(
mock DeckConfigurationDiscardChangesModal
) - when(mockUseCreateDeckConfigurationMutation).mockReturnValue({ - createDeckConfiguration: mockCreateDeckConfiguration, + when(mockUseUpdateDeckConfigurationMutation).mockReturnValue({ + updateDeckConfiguration: mockUpdateDeckConfiguration, } as any) }) diff --git a/app/src/pages/DeckConfiguration/index.tsx b/app/src/pages/DeckConfiguration/index.tsx index 8f1e84f8068..6ea37eb40d6 100644 --- a/app/src/pages/DeckConfiguration/index.tsx +++ b/app/src/pages/DeckConfiguration/index.tsx @@ -12,9 +12,13 @@ import { } from '@opentrons/components' import { useDeckConfigurationQuery, - useCreateDeckConfigurationMutation, + useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' -import { STANDARD_SLOT_LOAD_NAME } from '@opentrons/shared-data' +import { + SINGLE_RIGHT_CUTOUTS, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, +} from '@opentrons/shared-data' import { SmallButton } from '../../atoms/buttons' import { ChildNavigation } from '../../organisms/ChildNavigation' @@ -23,7 +27,7 @@ import { DeckFixtureSetupInstructionsModal } from '../../organisms/DeviceDetails import { DeckConfigurationDiscardChangesModal } from '../../organisms/DeviceDetailsDeckConfiguration/DeckConfigurationDiscardChangesModal' import { Portal } from '../../App/portal' -import type { Cutout, DeckConfiguration } from '@opentrons/shared-data' +import type { CutoutId, DeckConfiguration } from '@opentrons/shared-data' export function DeckConfigurationEditor(): JSX.Element { const { t, i18n } = useTranslation([ @@ -40,42 +44,45 @@ export function DeckConfigurationEditor(): JSX.Element { showConfigurationModal, setShowConfigurationModal, ] = React.useState(false) - const [ - targetFixtureLocation, - setTargetFixtureLocation, - ] = React.useState(null) + const [targetCutoutId, setTargetCutoutId] = React.useState( + null + ) const [ showDiscardChangeModal, setShowDiscardChangeModal, ] = React.useState(false) const deckConfig = useDeckConfigurationQuery().data ?? [] - const { createDeckConfiguration } = useCreateDeckConfigurationMutation() + const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() const [ currentDeckConfig, setCurrentDeckConfig, ] = React.useState(deckConfig) - const handleClickAdd = (fixtureLocation: Cutout): void => { - setTargetFixtureLocation(fixtureLocation) + const handleClickAdd = (cutoutId: CutoutId): void => { + setTargetCutoutId(cutoutId) setShowConfigurationModal(true) } - const handleClickRemove = (fixtureLocation: Cutout): void => { + const handleClickRemove = (cutoutId: CutoutId): void => { setCurrentDeckConfig(prevDeckConfig => prevDeckConfig.map(fixture => - fixture.fixtureLocation === fixtureLocation - ? { ...fixture, loadName: STANDARD_SLOT_LOAD_NAME } + fixture.cutoutId === cutoutId + ? { + ...fixture, + cutoutFixtureId: SINGLE_RIGHT_CUTOUTS.includes(cutoutId) + ? SINGLE_RIGHT_SLOT_FIXTURE + : SINGLE_LEFT_SLOT_FIXTURE, + } : fixture ) ) - createDeckConfiguration(currentDeckConfig) } const handleClickConfirm = (): void => { if (!isEqual(deckConfig, currentDeckConfig)) { - createDeckConfiguration(currentDeckConfig) + updateDeckConfiguration(currentDeckConfig) } history.goBack() } @@ -114,9 +121,9 @@ export function DeckConfigurationEditor(): JSX.Element { isOnDevice /> ) : null} - {showConfigurationModal && targetFixtureLocation != null ? ( + {showConfigurationModal && targetCutoutId != null ? (
) diff --git a/app/src/pages/Devices/ProtocolRunDetails/index.tsx b/app/src/pages/Devices/ProtocolRunDetails/index.tsx index 99852cd77d0..7402f77ab87 100644 --- a/app/src/pages/Devices/ProtocolRunDetails/index.tsx +++ b/app/src/pages/Devices/ProtocolRunDetails/index.tsx @@ -300,7 +300,6 @@ const ModuleControlsTab = ( const { t } = useTranslation('run_details') const currentRunId = useCurrentRunId() const moduleRenderInfoForProtocolById = useModuleRenderInfoForProtocolById( - robotName, runId ) const { isRunStill } = useRunStatuses() diff --git a/app/src/pages/OnDeviceDisplay/ProtocolDetails/Hardware.tsx b/app/src/pages/OnDeviceDisplay/ProtocolDetails/Hardware.tsx index 5ec14ce19ff..0f4a8fb94a1 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolDetails/Hardware.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolDetails/Hardware.tsx @@ -14,6 +14,7 @@ import { } from '@opentrons/components' import { GRIPPER_V1, + getCutoutDisplayName, getGripperDisplayName, getModuleDisplayName, getModuleType, @@ -82,7 +83,7 @@ const getHardwareName = (protocolHardware: ProtocolHardware): string => { } else if (protocolHardware.hardwareType === 'module') { return getModuleDisplayName(protocolHardware.moduleModel) } else { - return getFixtureDisplayName(protocolHardware.fixtureName) + return getFixtureDisplayName(protocolHardware.cutoutFixtureId) } } @@ -130,7 +131,11 @@ export const Hardware = (props: { protocolId: string }): JSX.Element => { if (hardware.hardwareType === 'module') { location = } else if (hardware.hardwareType === 'fixture') { - location = + location = ( + + ) } return ( diff --git a/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx b/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx index 7c538f79bc5..e70ebc86dde 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolDetails/__tests__/Hardware.test.tsx @@ -1,9 +1,9 @@ import * as React from 'react' import { when, resetAllWhenMocks } from 'jest-when' import { - STAGING_AREA_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, - WASTE_CHUTE_SLOT, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import { renderWithProviders } from '@opentrons/components' import { i18n } from '../../../../i18n' @@ -62,14 +62,14 @@ describe('Hardware', () => { }, { hardwareType: 'fixture', - fixtureName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: WASTE_CHUTE_SLOT }, + cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + location: { cutout: WASTE_CHUTE_CUTOUT }, hasSlotConflict: false, }, { hardwareType: 'fixture', - fixtureName: STAGING_AREA_LOAD_NAME, - location: { cutout: 'B3' }, + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + location: { cutout: 'cutoutB3' }, hasSlotConflict: false, }, ], @@ -93,7 +93,7 @@ describe('Hardware', () => { }) getByRole('row', { name: '1 Heater-Shaker Module GEN1' }) getByRole('row', { name: '3 Temperature Module GEN2' }) - getByRole('row', { name: 'D3 Waste Chute' }) - getByRole('row', { name: 'B3 Staging Area Slot' }) + getByRole('row', { name: 'D3 Waste chute only' }) + getByRole('row', { name: 'B3 Staging area slot' }) }) }) diff --git a/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index 16719381826..eff166f2617 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -18,7 +18,7 @@ import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__' import { FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - STAGING_AREA_LOAD_NAME, + STAGING_AREA_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import ot3StandardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot3_standard.json' @@ -46,6 +46,7 @@ import { useRunStatus, } from '../../../../organisms/RunTimeControl/hooks' import { useIsHeaterShakerInProtocol } from '../../../../organisms/ModuleCard/hooks' +import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks' import { ConfirmAttachedModal } from '../ConfirmAttachedModal' import { ProtocolSetup } from '..' @@ -53,7 +54,6 @@ import type { UseQueryResult } from 'react-query' import type { DeckConfiguration, CompletedProtocolAnalysis, - Fixture, } from '@opentrons/shared-data' // Mock IntersectionObserver @@ -88,6 +88,7 @@ jest.mock('../../../../organisms/ModuleCard/hooks') jest.mock('../../../../redux/discovery/selectors') jest.mock('../ConfirmAttachedModal') jest.mock('../../../../organisms/ToasterOven') +jest.mock('../../../../resources/deck_configuration/hooks') const mockGetDeckDefFromRobotType = getDeckDefFromRobotType as jest.MockedFunction< typeof getDeckDefFromRobotType @@ -163,6 +164,9 @@ const mockUseModuleCalibrationStatus = useModuleCalibrationStatus as jest.Mocked const mockGetLocalRobot = getLocalRobot as jest.MockedFunction< typeof getLocalRobot > +const mockUseDeckConfigurationCompatibility = useDeckConfigurationCompatibility as jest.MockedFunction< + typeof useDeckConfigurationCompatibility +> const render = (path = '/') => { return renderWithProviders( @@ -230,10 +234,9 @@ const mockDoorStatus = { }, } const mockFixture = { - fixtureId: 'mockId', - fixtureLocation: 'D1', - loadName: STAGING_AREA_LOAD_NAME, -} as Fixture + cutoutId: 'cutoutD1', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, +} const MOCK_MAKE_SNACKBAR = jest.fn() @@ -332,6 +335,7 @@ describe('ProtocolSetup', () => { .mockReturnValue(({ makeSnackbar: MOCK_MAKE_SNACKBAR, } as unknown) as any) + when(mockUseDeckConfigurationCompatibility).mockReturnValue([]) }) afterEach(() => { diff --git a/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx b/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx index d8732a22457..ea3210279e6 100644 --- a/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx +++ b/app/src/pages/OnDeviceDisplay/ProtocolSetup/index.tsx @@ -81,7 +81,7 @@ import { ConfirmAttachedModal } from './ConfirmAttachedModal' import { getLatestCurrentOffsets } from '../../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils' import { CloseButton, PlayButton } from './Buttons' -import type { Cutout, FixtureLoadName } from '@opentrons/shared-data' +import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data' import type { OnDeviceRouteParams } from '../../../App/types' import type { ProtocolHardware, ProtocolFixture } from '../../Protocols/hooks' import type { ProtocolModuleInfo } from '../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' @@ -463,7 +463,7 @@ function PrepareToRun({ const missingFixturesText = missingFixtures.length === 1 ? `${t('missing')} ${getFixtureDisplayName( - missingFixtures[0].fixtureName + missingFixtures[0].cutoutFixtureId )}` : t('multiple_fixtures_missing', { count: missingFixtures.length }) @@ -691,11 +691,9 @@ export function ProtocolSetup(): JSX.Element { handleProceedToRunClick, !configBypassHeaterShakerAttachmentConfirmation ) - const [fixtureLocation, setFixtureLocation] = React.useState( - '' as Cutout - ) + const [cutoutId, setCutoutId] = React.useState(null) const [providedFixtureOptions, setProvidedFixtureOptions] = React.useState< - FixtureLoadName[] + CutoutFixtureId[] >([]) // orchestrate setup subpages/components @@ -719,7 +717,7 @@ export function ProtocolSetup(): JSX.Element { ), @@ -731,7 +729,7 @@ export function ProtocolSetup(): JSX.Element { ), 'deck configuration': ( } else if (recentRunsOfUniqueProtocols.length > 0) { contents = ( diff --git a/app/src/pages/ProtocolDashboard/index.tsx b/app/src/pages/ProtocolDashboard/index.tsx index 4c64285667d..fd11c3e2192 100644 --- a/app/src/pages/ProtocolDashboard/index.tsx +++ b/app/src/pages/ProtocolDashboard/index.tsx @@ -96,7 +96,14 @@ export function ProtocolDashboard(): JSX.Element { } const runData = runs.data?.data != null ? runs.data?.data : [] - const sortedProtocols = sortProtocols(sortBy, unpinnedProtocols, runData) + const allRunsNewestFirst = runData.sort( + (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ) + const sortedProtocols = sortProtocols( + sortBy, + unpinnedProtocols, + allRunsNewestFirst + ) const handleProtocolsBySortKey = ( sortKey: ProtocolsOnDeviceSortKey ): void => { diff --git a/app/src/pages/ProtocolDashboard/utils.ts b/app/src/pages/ProtocolDashboard/utils.ts index 74ee9a8f3b7..cdf873b4298 100644 --- a/app/src/pages/ProtocolDashboard/utils.ts +++ b/app/src/pages/ProtocolDashboard/utils.ts @@ -7,17 +7,17 @@ const DUMMY_FOR_NO_DATE_CASE = -8640000000000000 export function sortProtocols( sortBy: ProtocolsOnDeviceSortKey, protocols: ProtocolResource[], - runs: RunData[] + allRunsNewestFirst: RunData[] ): ProtocolResource[] { protocols.sort((a, b) => { const aName = a.metadata.protocolName ?? a.files[0].name const bName = b.metadata.protocolName ?? b.files[0].name const aLastRun = new Date( - runs.find(run => run.protocolId === a.id)?.completedAt ?? + allRunsNewestFirst.find(run => run.protocolId === a.id)?.completedAt ?? new Date(DUMMY_FOR_NO_DATE_CASE) ) const bLastRun = new Date( - runs.find(run => run.protocolId === b.id)?.completedAt ?? + allRunsNewestFirst.find(run => run.protocolId === b.id)?.completedAt ?? new Date(DUMMY_FOR_NO_DATE_CASE) ) const aDate = new Date(a.createdAt) diff --git a/app/src/pages/Protocols/hooks/index.ts b/app/src/pages/Protocols/hooks/index.ts index c5232d29f24..60d58e660b1 100644 --- a/app/src/pages/Protocols/hooks/index.ts +++ b/app/src/pages/Protocols/hooks/index.ts @@ -6,14 +6,19 @@ import { useProtocolAnalysisAsDocumentQuery, useProtocolQuery, } from '@opentrons/react-api-client' -import { STANDARD_SLOT_LOAD_NAME } from '@opentrons/shared-data' +import { + FLEX_ROBOT_TYPE, + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS, + SINGLE_SLOT_FIXTURES, +} from '@opentrons/shared-data' import { getLabwareSetupItemGroups } from '../utils' import { getProtocolUsesGripper } from '../../../organisms/ProtocolSetupInstruments/utils' +import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks' import type { CompletedProtocolAnalysis, - Cutout, - FixtureLoadName, + CutoutFixtureId, + CutoutId, ModuleModel, PipetteName, } from '@opentrons/shared-data' @@ -42,8 +47,8 @@ interface ProtocolGripper { export interface ProtocolFixture { hardwareType: 'fixture' - fixtureName: FixtureLoadName - location: { cutout: Cutout } + cutoutFixtureId: CutoutFixtureId | null + location: { cutout: CutoutId } hasSlotConflict: boolean } @@ -69,6 +74,10 @@ export const useRequiredProtocolHardwareFromAnalysis = ( const attachedInstruments = attachedInstrumentsData?.data ?? [] const { data: deckConfig } = useDeckConfigurationQuery() + const deckConfigCompatibility = useDeckConfigurationCompatibility( + FLEX_ROBOT_TYPE, + analysis?.commands ?? [] + ) if (analysis == null || analysis?.status !== 'completed') { return { requiredProtocolHardware: [], isLoading: true } @@ -103,11 +112,13 @@ export const useRequiredProtocolHardwareFromAnalysis = ( moduleModel: model, slot: location.slotName, connected: handleModuleConnectionCheckFor(attachedModules, model), - hasSlotConflict: !!deckConfig?.find( - fixture => - fixture.fixtureLocation === location.slotName && - fixture.loadName !== STANDARD_SLOT_LOAD_NAME - ), + hasSlotConflict: + deckConfig?.find( + ({ cutoutId, cutoutFixtureId }) => + cutoutId === location.slotName && + cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + ) != null, } } ) @@ -128,48 +139,37 @@ export const useRequiredProtocolHardwareFromAnalysis = ( }) ) - // TODO(jr, 10/2/23): IMMEDIATELY delete the stubs when api supports - // loadFixture - // const requiredFixture: ProtocolFixture[] = analysis.commands - // .filter( - // (command): command is LoadFixtureRunTimeCommand => - // command.commandType === 'loadFixture' - // ) - // .map(({ params }) => { - // return { - // hardwareType: 'fixture', - // fixtureName: params.loadName, - // location: params.location, - // } - // }) - const STUBBED_FIXTURES: ProtocolFixture[] = [ - { - hardwareType: 'fixture', - fixtureName: 'wasteChute', - location: { cutout: 'D3' }, - hasSlotConflict: false, - }, - { - hardwareType: 'fixture', - fixtureName: 'standardSlot', - location: { cutout: 'C3' }, - hasSlotConflict: false, - }, - { - hardwareType: 'fixture', - fixtureName: 'stagingArea', - location: { cutout: 'B3' }, - hasSlotConflict: false, - }, - ] + const nonSingleSlotDeckConfigCompatibility = deckConfigCompatibility.filter( + ({ requiredAddressableAreas }) => + // required AA list includes a non-single-slot AA + !requiredAddressableAreas.every(aa => + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS.includes(aa) + ) + ) + // fixture includes at least 1 required AA + const requiredDeckConfigCompatibility = nonSingleSlotDeckConfigCompatibility.filter( + fixture => fixture.requiredAddressableAreas.length > 0 + ) + + const requiredFixtures = requiredDeckConfigCompatibility.map( + ({ cutoutFixtureId, cutoutId, compatibleCutoutFixtureIds }) => ({ + hardwareType: 'fixture' as const, + cutoutFixtureId, + location: { cutout: cutoutId }, + hasSlotConflict: + cutoutFixtureId != null && + !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + ? compatibleCutoutFixtureIds.includes(cutoutFixtureId) + : false, + }) + ) return { requiredProtocolHardware: [ ...requiredPipettes, ...requiredModules, ...requiredGripper, - // ...requiredFixture, - ...STUBBED_FIXTURES, + ...requiredFixtures, ], isLoading: isLoadingInstruments || isLoadingModules, } @@ -245,10 +245,10 @@ const useMissingProtocolHardwareFromRequiredProtocolHardware = ( return !hardware.connected } else { // fixtures - return !deckConfig?.find( - fixture => - hardware.location.cutout === fixture.fixtureLocation && - hardware.fixtureName === fixture.loadName + return !deckConfig?.some( + ({ cutoutId, cutoutFixtureId }) => + hardware.location.cutout === cutoutId && + hardware.cutoutFixtureId === cutoutFixtureId ) } }), diff --git a/app/src/redux/analytics/__tests__/make-event.test.ts b/app/src/redux/analytics/__tests__/make-event.test.ts index 6a149c81d87..c7906c72de8 100644 --- a/app/src/redux/analytics/__tests__/make-event.test.ts +++ b/app/src/redux/analytics/__tests__/make-event.test.ts @@ -14,18 +14,6 @@ const getAnalyticsSessionExitDetails = selectors.getAnalyticsSessionExitDetails const getSessionInstrumentAnalyticsData = selectors.getSessionInstrumentAnalyticsData as jest.MockedFunction< typeof selectors.getSessionInstrumentAnalyticsData > -const getAnalyticsHealthCheckData = selectors.getAnalyticsHealthCheckData as jest.MockedFunction< - typeof selectors.getAnalyticsHealthCheckData -> -const getAnalyticsDeckCalibrationData = selectors.getAnalyticsDeckCalibrationData as jest.MockedFunction< - typeof selectors.getAnalyticsDeckCalibrationData -> -const getAnalyticsPipetteCalibrationData = selectors.getAnalyticsPipetteCalibrationData as jest.MockedFunction< - typeof selectors.getAnalyticsPipetteCalibrationData -> -const getAnalyticsTipLengthCalibrationData = selectors.getAnalyticsTipLengthCalibrationData as jest.MockedFunction< - typeof selectors.getAnalyticsTipLengthCalibrationData -> describe('analytics events map', () => { beforeEach(() => { @@ -63,18 +51,10 @@ describe('analytics events map', () => { someStuff: 'some-other-stuff', }, } as any - getAnalyticsPipetteCalibrationData.mockReturnValue({ - markedBad: true, - calibrationExists: true, - pipetteModel: 'my pipette model', - }) return expect(makeEvent(action, state)).resolves.toEqual({ name: 'pipetteOffsetCalibrationStarted', properties: { ...action.payload, - calibrationExists: true, - markedBad: true, - pipetteModel: 'my pipette model', }, }) }) @@ -87,76 +67,14 @@ describe('analytics events map', () => { someStuff: 'some-other-stuff', }, } as any - getAnalyticsTipLengthCalibrationData.mockReturnValue({ - markedBad: true, - calibrationExists: true, - pipetteModel: 'pipette-model', - }) return expect(makeEvent(action, state)).resolves.toEqual({ name: 'tipLengthCalibrationStarted', properties: { ...action.payload, - calibrationExists: true, - markedBad: true, - pipetteModel: 'pipette-model', - }, - }) - }) - - it('sessions:ENSURE_SESSION for deck cal -> deckCalibrationStarted event', () => { - const state = {} as any - const action = { - type: 'sessions:ENSURE_SESSION', - payload: { - sessionType: 'deckCalibration', - }, - } as any - getAnalyticsDeckCalibrationData.mockReturnValue({ - calibrationStatus: 'IDENTITY', - markedBad: true, - pipettes: { left: { model: 'my pipette model' } }, - } as any) - - return expect(makeEvent(action, state)).resolves.toEqual({ - name: 'deckCalibrationStarted', - properties: { - calibrationStatus: 'IDENTITY', - markedBad: true, - pipettes: { left: { model: 'my pipette model' } }, }, }) }) - it('sessions:ENSURE_SESSION for health check -> calibrationHealthCheckStarted event', () => { - const state = {} as any - const action = { - type: 'sessions:ENSURE_SESSION', - payload: { - sessionType: 'calibrationCheck', - }, - } as any - getAnalyticsHealthCheckData.mockReturnValue({ - pipettes: { left: { model: 'my pipette model' } }, - } as any) - return expect(makeEvent(action, state)).resolves.toEqual({ - name: 'calibrationHealthCheckStarted', - properties: { - pipettes: { left: { model: 'my pipette model' } }, - }, - }) - }) - - it('sessions:ENSURE_SESSION for other session -> no event', () => { - const state = {} as any - const action = { - type: 'sessions:ENSURE_SESSION', - payload: { - sessionType: 'some-other-session', - }, - } as any - return expect(makeEvent(action, state)).resolves.toBeNull() - }) - it('sessions:CREATE_SESSION_COMMAND for exit -> {type}Exit', () => { const state = {} as any const action = { diff --git a/app/src/redux/analytics/__tests__/selectors.test.ts b/app/src/redux/analytics/__tests__/selectors.test.ts index 7bbd0b296a7..63b539748d8 100644 --- a/app/src/redux/analytics/__tests__/selectors.test.ts +++ b/app/src/redux/analytics/__tests__/selectors.test.ts @@ -56,12 +56,6 @@ describe('analytics selectors', () => { }) describe('analytics calibration selectors', () => { - describe('getAnalyticsHealthCheckData', () => { - it('should return null if no robot connected', () => { - const mockState: State = {} as any - expect(Selectors.getAnalyticsHealthCheckData(mockState)).toBeNull() - }) - }) describe('getAnalyticsSessionExitDetails', () => { const mockGetRobotSessionById = SessionsSelectors.getRobotSessionById as jest.MockedFunction< typeof SessionsSelectors.getRobotSessionById diff --git a/app/src/redux/analytics/make-event.ts b/app/src/redux/analytics/make-event.ts index 5b38f947073..8fdede1dbf2 100644 --- a/app/src/redux/analytics/make-event.ts +++ b/app/src/redux/analytics/make-event.ts @@ -1,8 +1,4 @@ // redux action types to analytics events map -// TODO(mc, 2022-03-04): large chunks of this module are commented -// out because RPC-based analytics events were not replaced with -// the switch to the HTTP APIs. Commented out code left to aid with -// analytics replacement. import * as CustomLabware from '../custom-labware' import * as SystemInfo from '../system-info' import * as RobotUpdate from '../robot-update/constants' @@ -14,17 +10,12 @@ import * as RobotAdmin from '../robot-admin' import { getBuildrootAnalyticsData, - getAnalyticsPipetteCalibrationData, - getAnalyticsTipLengthCalibrationData, - getAnalyticsHealthCheckData, - getAnalyticsDeckCalibrationData, getAnalyticsSessionExitDetails, getSessionInstrumentAnalyticsData, } from './selectors' import type { State, Action } from '../types' import type { AnalyticsEvent } from './types' -import type { Mount } from '../pipettes/types' const EVENT_APP_UPDATE_DISMISSED = 'appUpdateDismissed' @@ -33,111 +24,6 @@ export function makeEvent( state: State ): Promise { switch (action.type) { - // case 'robot:CONNECT': { - // const robot = getConnectedRobot(state) - - // if (!robot) { - // log.warn('No robot found for connect response') - // return Promise.resolve(null) - // } - - // const data = getRobotAnalyticsData(state) - - // return Promise.resolve({ - // name: 'robotConnect', - // properties: { - // ...data, - // method: robot.local ? 'usb' : 'wifi', - // success: true, - // }, - // }) - // } - - // case 'protocol:LOAD': { - // return getProtocolAnalyticsData(state).then(data => ({ - // name: 'protocolUploadRequest', - // properties: { - // ...getRobotAnalyticsData(state), - // ...data, - // }, - // })) - // } - - // case 'robot:SESSION_RESPONSE': - // case 'robot:SESSION_ERROR': { - // // only fire event if we had a protocol upload in flight; we don't want - // // to fire if user connects to robot with protocol already loaded - // const { type: actionType, payload: actionPayload, meta } = action - // if (!meta.freshUpload) return Promise.resolve(null) - - // return getProtocolAnalyticsData(state).then(data => ({ - // name: 'protocolUploadResponse', - // properties: { - // ...getRobotAnalyticsData(state), - // ...data, - // success: actionType === 'robot:SESSION_RESPONSE', - // // @ts-expect-error even if we used the in operator, TS cant narrow error to anything more specific than 'unknown' https://github.com/microsoft/TypeScript/issues/25720 - // error: (actionPayload.error && actionPayload.error.message) || '', - // }, - // })) - // } - - // case 'robot:RUN': { - // return getProtocolAnalyticsData(state).then(data => ({ - // name: 'runStart', - // properties: { - // ...getRobotAnalyticsData(state), - // ...data, - // }, - // })) - // } - - // TODO(mc, 2019-01-22): we only get this event if the user keeps their app - // open for the entire run. Fixing this is blocked until we can fix - // session.stop from triggering a run error - // case 'robot:RUN_RESPONSE': { - // const runTime = robotSelectors.getRunSeconds(state) - // const success = !action.error - // const error = action.error ? action.payload?.message || '' : '' - - // return getProtocolAnalyticsData(state).then(data => ({ - // name: 'runFinish', - // properties: { - // ...getRobotAnalyticsData(state), - // ...data, - // runTime, - // success, - // error, - // }, - // })) - // } - - // case 'robot:PAUSE': { - // const runTime = robotSelectors.getRunSeconds(state) - - // return getProtocolAnalyticsData(state).then(data => ({ - // name: 'runPause', - // properties: { ...data, runTime }, - // })) - // } - - // case 'robot:RESUME': { - // const runTime = robotSelectors.getRunSeconds(state) - - // return getProtocolAnalyticsData(state).then(data => ({ - // name: 'runResume', - // properties: { ...data, runTime }, - // })) - // } - - // case 'robot:CANCEL': - // const runTime = robotSelectors.getRunSeconds(state) - - // return getProtocolAnalyticsData(state).then(data => ({ - // name: 'runCancel', - // properties: { ...data, runTime }, - // })) - // robot update events case RobotUpdate.ROBOTUPDATE_SET_UPDATE_SEEN: { const data = getBuildrootAnalyticsData(state, action.meta.robotName) @@ -265,7 +151,7 @@ export function makeEvent( const systemInfoProps = SystemInfo.getU2EDeviceAnalyticsProps(state) return Promise.resolve( - systemInfoProps + systemInfoProps != null ? { superProperties: { ...systemInfoProps, @@ -280,43 +166,16 @@ export function makeEvent( ) } - case Sessions.ENSURE_SESSION: { - switch (action.payload.sessionType) { - case Sessions.SESSION_TYPE_DECK_CALIBRATION: - const dcAnalyticsProps = getAnalyticsDeckCalibrationData(state) - return Promise.resolve( - dcAnalyticsProps - ? { - name: 'deckCalibrationStarted', - properties: dcAnalyticsProps, - } - : null - ) - case Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK: - const hcAnalyticsProps = getAnalyticsHealthCheckData(state) - return Promise.resolve( - hcAnalyticsProps - ? { - name: 'calibrationHealthCheckStarted', - properties: hcAnalyticsProps, - } - : null - ) - default: - return Promise.resolve(null) - } - } - case Sessions.CREATE_SESSION_COMMAND: { switch (action.payload.command.command) { - case sharedCalCommands.EXIT: + case sharedCalCommands.EXIT: { const sessionDetails = getAnalyticsSessionExitDetails( state, action.payload.robotName, action.payload.sessionId ) return Promise.resolve( - sessionDetails + sessionDetails != null ? { name: `${sessionDetails.sessionType}Exit`, properties: { @@ -325,25 +184,25 @@ export function makeEvent( } : null ) - case sharedCalCommands.LOAD_LABWARE: + } + case sharedCalCommands.LOAD_LABWARE: { const commandData = action.payload.command.data - if (commandData) { + if (commandData != null) { const instrData = getSessionInstrumentAnalyticsData( state, action.payload.robotName, action.payload.sessionId ) return Promise.resolve( - instrData + instrData != null ? { name: `${instrData.sessionType}TipRackSelect`, properties: { pipetteModel: instrData.pipetteModel, - // @ts-expect-error TODO: use in operator and add test case for no tiprackDefiniton on CommandData - tipRackDisplayName: commandData.tiprackDefinition - ? // @ts-expect-error TODO: use in operator and add test case for no tiprackDefiniton on CommandData - commandData.tiprackDefinition.metadata.displayName - : null, + tipRackDisplayName: + 'tiprackDefinition' in commandData + ? commandData.tiprackDefinition.metadata.displayName + : null, }, } : null @@ -351,6 +210,7 @@ export function makeEvent( } else { return Promise.resolve(null) } + } default: return Promise.resolve(null) } @@ -374,10 +234,6 @@ export function makeEvent( name: 'pipetteOffsetCalibrationStarted', properties: { ...action.payload, - ...getAnalyticsPipetteCalibrationData( - state, - action.payload.mount as Mount - ), }, }) } @@ -387,10 +243,6 @@ export function makeEvent( name: 'tipLengthCalibrationStarted', properties: { ...action.payload, - ...getAnalyticsTipLengthCalibrationData( - state, - action.payload.mount as Mount - ), }, }) } diff --git a/app/src/redux/analytics/selectors.ts b/app/src/redux/analytics/selectors.ts index d0362f77348..fcb9ab18a2d 100644 --- a/app/src/redux/analytics/selectors.ts +++ b/app/src/redux/analytics/selectors.ts @@ -1,16 +1,5 @@ -// import { createSelector } from 'reselect' import * as Sessions from '../sessions' -// import { -// getProtocolType, -// getProtocolCreatorApp, -// getProtocolApiVersion, -// getProtocolName, -// getProtocolSource, -// getProtocolAuthor, -// getProtocolContents, -// } from '../protocol' - import { getViewableRobots, getRobotApiVersion } from '../discovery' import { @@ -22,127 +11,37 @@ import { import { getRobotSessionById } from '../sessions/selectors' -// import { hash } from './hash' - -// import type { Selector } from 'reselect' import type { State } from '../types' -import type { Mount } from '../pipettes/types' import type { AnalyticsConfig, BuildrootAnalyticsData, - PipetteOffsetCalibrationAnalyticsData, - TipLengthCalibrationAnalyticsData, - DeckCalibrationAnalyticsData, - CalibrationHealthCheckAnalyticsData, AnalyticsSessionExitDetails, SessionInstrumentAnalyticsData, } from './types' -export const FF_PREFIX = 'robotFF_' - -// const _getUnhashedProtocolAnalyticsData: Selector< -// State, -// ProtocolAnalyticsData -// > = createSelector( -// getProtocolType, -// getProtocolCreatorApp, -// getProtocolApiVersion, -// getProtocolName, -// getProtocolSource, -// getProtocolAuthor, -// getProtocolContents, -// getPipettes, -// getModules, -// ( -// type, -// app, -// apiVersion, -// name, -// source, -// author, -// contents, -// pipettes, -// modules -// ) => ({ -// protocolType: type || '', -// protocolAppName: app.name || '', -// protocolAppVersion: app.version || '', -// protocolApiVersion: apiVersion || '', -// protocolName: name || '', -// protocolSource: source || '', -// protocolAuthor: author || '', -// protocolText: contents || '', -// pipettes: pipettes.map(p => p.requestedAs ?? p.name).join(','), -// modules: modules.map(m => m.model).join(','), -// }) -// ) - -// export const getProtocolAnalyticsData: ( -// state: State -// ) => Promise = createSelector< -// State, -// ProtocolAnalyticsData, -// Promise -// >(_getUnhashedProtocolAnalyticsData, (data: ProtocolAnalyticsData) => { -// const hashTasks = [hash(data.protocolAuthor), hash(data.protocolText)] - -// return Promise.all(hashTasks).then(([protocolAuthor, protocolText]) => ({ -// ...data, -// protocolAuthor: data.protocolAuthor !== '' ? protocolAuthor : '', -// protocolText: data.protocolText !== '' ? protocolText : '', -// })) -// }) - -// export function getRobotAnalyticsData(state: State): RobotAnalyticsData | null { -// const robot = getConnectedRobot(state) - -// if (robot) { -// const pipettes = getAttachedPipettes(state, robot.name) -// const settings = getRobotSettings(state, robot.name) - -// // @ts-expect-error RobotAnalyticsData type needs boolean values should it be boolean | string -// return settings.reduce( -// (result, setting) => ({ -// ...result, -// [`${FF_PREFIX}${setting.id}`]: !!setting.value, -// }), -// // @ts-expect-error RobotAnalyticsData type needs boolean values should it be boolean | string -// { -// robotApiServerVersion: getRobotApiVersion(robot) || '', -// robotSmoothieVersion: getRobotFirmwareVersion(robot) || '', -// robotLeftPipette: pipettes.left?.model || '', -// robotRightPipette: pipettes.right?.model || '', -// } -// ) -// } - -// return null -// } - export function getBuildrootAnalyticsData( state: State, robotName: string | null = null ): BuildrootAnalyticsData | null { - const updateVersion = robotName - ? getRobotUpdateVersion(state, robotName) - : null + const updateVersion = + robotName != null ? getRobotUpdateVersion(state, robotName) : null const session = getRobotUpdateSession(state) const robot = robotName === null ? getRobotUpdateRobot(state) - : getViewableRobots(state).find(r => r.name === robotName) || null + : getViewableRobots(state).find(r => r.name === robotName) ?? null if (updateVersion === null || robot === null) return null - const currentVersion = getRobotApiVersion(robot) || 'unknown' - const currentSystem = getRobotSystemType(robot) || 'unknown' + const currentVersion = getRobotApiVersion(robot) ?? 'unknown' + const currentSystem = getRobotSystemType(robot) ?? 'unknown' return { currentVersion, currentSystem, updateVersion, - error: session?.error || null, + error: session != null && 'error' in session ? session.error : null, } } @@ -158,132 +57,13 @@ export function getAnalyticsOptInSeen(state: State): boolean { return state.config?.analytics.seenOptIn ?? true } -export function getAnalyticsPipetteCalibrationData( - state: State, - mount: Mount -): PipetteOffsetCalibrationAnalyticsData | null { - // const robot = getConnectedRobot(state) - - // if (robot) { - // const pipcal = - // getAttachedPipetteCalibrations(state, robot.name)[mount]?.offset ?? null - // const pip = getAttachedPipettes(state, robot.name)[mount] - // return { - // calibrationExists: Boolean(pipcal), - // markedBad: pipcal?.status?.markedBad ?? false, - // // @ts-expect-error protect for cases where model is not on pip - // pipetteModel: pip.model, - // } - // } - return null -} - -export function getAnalyticsTipLengthCalibrationData( - state: State, - mount: Mount -): TipLengthCalibrationAnalyticsData | null { - // const robot = getConnectedRobot(state) - - // if (robot) { - // const tipcal = - // getAttachedPipetteCalibrations(state, robot.name)[mount]?.tipLength ?? - // null - // const pip = getAttachedPipettes(state, robot.name)[mount] - // return { - // calibrationExists: Boolean(tipcal), - // markedBad: tipcal?.status?.markedBad ?? false, - // // @ts-expect-error protect for cases where model is not on pip - // pipetteModel: pip.model, - // } - // } - return null -} - -// function getPipetteModels(state: State, robotName: string): ModelsByMount { -// // @ts-expect-error ensure that both mount keys exist on returned object -// return Object.entries( -// getAttachedPipettes(state, robotName) -// ).reduce((obj, [mount, pipData]): ModelsByMount => { -// if (pipData) { -// obj[mount as Mount] = pick(pipData, ['model']) -// } -// return obj -// // @ts-expect-error ensure that both mount keys exist on returned object -// }, {}) -// } - -// function getCalibrationCheckData( -// state: State, -// robotName: string -// ): CalibrationCheckByMount | null { -// const session = getCalibrationCheckSession(state, robotName) -// if (!session) { -// return null -// } -// const { comparisonsByPipette, instruments } = session.details -// return instruments.reduce( -// (obj, instrument: CalibrationCheckInstrument) => { -// const { rank, mount, model } = instrument -// const succeeded = !some( -// Object.keys(comparisonsByPipette[rank]).map(k => -// Boolean( -// comparisonsByPipette[rank][ -// k as keyof CalibrationCheckComparisonsPerCalibration -// ]?.status === 'OUTSIDE_THRESHOLD' -// ) -// ) -// ) -// obj[mount] = { -// comparisons: comparisonsByPipette[rank], -// succeeded: succeeded, -// model: model, -// } -// return obj -// }, -// { left: null, right: null } -// ) -// } - -export function getAnalyticsDeckCalibrationData( - state: State -): DeckCalibrationAnalyticsData | null { - // TODO(va, 08-17-22): this selector was broken and was always returning null because getConnectedRobot - // always returned null, this should be fixed at the epic level in a future ticket RAUT-150 - // const robot = getConnectedRobot(state) - // if (robot) { - // const dcData = getDeckCalibrationData(state, robot.name) - // return { - // calibrationStatus: getDeckCalibrationStatus(state, robot.name), - // markedBad: !Array.isArray(dcData) - // ? dcData?.status?.markedBad || null - // : null, - // pipettes: getPipetteModels(state, robot.name), - // } - // } - return null -} - -export function getAnalyticsHealthCheckData( - state: State -): CalibrationHealthCheckAnalyticsData | null { - // TODO(va, 08-17-22): this selector was broken and was always returning null because getConnectedRobot - // always returned null, this should be fixed at the epic level in a future ticket RAUT-150 - // const robot = getConnectedRobot(state) - // if (robot) { - // return { - // pipettes: getCalibrationCheckData(state, robot.name), - // } - // } - return null -} - export function getAnalyticsSessionExitDetails( state: State, robotName: string, sessionId: string ): AnalyticsSessionExitDetails | null { const session = getRobotSessionById(state, robotName, sessionId) - if (session) { + if (session != null) { return { step: session.details.currentStep, sessionType: session.sessionType, @@ -298,7 +78,7 @@ export function getSessionInstrumentAnalyticsData( sessionId: string ): SessionInstrumentAnalyticsData | null { const session = getRobotSessionById(state, robotName, sessionId) - if (session) { + if (session != null) { const pipModel = session.sessionType === Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK ? session.details.activePipette.model diff --git a/app/src/redux/robot-api/http.ts b/app/src/redux/robot-api/http.ts index 71840066a9f..7dd41b87da6 100644 --- a/app/src/redux/robot-api/http.ts +++ b/app/src/redux/robot-api/http.ts @@ -60,6 +60,7 @@ export function fetchRobotApi( headers: options.headers, method, url, + data: options.body, }) ).pipe( map(response => ({ diff --git a/app/src/redux/robot-update/__tests__/epic.test.ts b/app/src/redux/robot-update/__tests__/epic.test.ts index 92172ec3852..91141fa75ab 100644 --- a/app/src/redux/robot-update/__tests__/epic.test.ts +++ b/app/src/redux/robot-update/__tests__/epic.test.ts @@ -386,6 +386,46 @@ describe('robot update epics', () => { }) }) + describe('startUpdateAfterFileDownload', () => { + it('should start the update after file download if the robot is a flex', () => { + testScheduler.run(({ hot, cold, expectObservable }) => { + const session: ReturnType = { + stage: 'done', + step: 'downloadFile', + } as any + + getRobotUpdateRobot.mockReturnValue(brRobotFlex) + getRobotUpdateSession.mockReturnValue(session) + + const state$ = hot('-a', { a: state }) + const output$ = epics.startUpdateAfterFileDownload(null as any, state$) + + expectObservable(output$).toBe('-a', { + a: actions.readSystemRobotUpdateFile('flex'), + }) + }) + }) + + it('should start the update after file download if the robot is a ot-2', () => { + testScheduler.run(({ hot, cold, expectObservable }) => { + const session: ReturnType = { + stage: 'done', + step: 'downloadFile', + } as any + + getRobotUpdateRobot.mockReturnValue(brRobotOt2) + getRobotUpdateSession.mockReturnValue(session) + + const state$ = hot('-a', { a: state }) + const output$ = epics.startUpdateAfterFileDownload(null as any, state$) + + expectObservable(output$).toBe('-a', { + a: actions.readSystemRobotUpdateFile('ot2'), + }) + }) + }) + }) + it('retryAfterPremigrationEpic', () => { testScheduler.run(({ hot, expectObservable }) => { getRobotUpdateRobot.mockReturnValueOnce(brReadyRobot) diff --git a/app/src/redux/robot-update/__tests__/reducer.test.ts b/app/src/redux/robot-update/__tests__/reducer.test.ts index 05a38d7b303..f681706c2b6 100644 --- a/app/src/redux/robot-update/__tests__/reducer.test.ts +++ b/app/src/redux/robot-update/__tests__/reducer.test.ts @@ -137,6 +137,69 @@ describe('robot update reducer', () => { }, }, }, + { + name: 'handles robotUpdate:CHECKING_FOR_UPDATE', + action: { + type: 'robotUpdate:CHECKING_FOR_UPDATE', + payload: 'ot2', + }, + initialState: { ...INITIAL_STATE, session: null }, + expected: { + ...INITIAL_STATE, + session: { + step: 'downloadFile', + stage: 'writing', + target: 'ot2', + }, + }, + }, + { + name: + 'handles robotUpdate:DOWNLOAD_DONE when the target matches the robot type', + action: { + type: 'robotUpdate:DOWNLOAD_DONE', + payload: 'ot2', + }, + initialState: { + ...INITIAL_STATE, + session: { + step: 'downloadFile', + stage: 'writing', + target: 'ot2', + }, + }, + expected: { + ...INITIAL_STATE, + session: { + step: 'downloadFile', + stage: 'done', + }, + }, + }, + { + name: + 'handles robotUpdate:DOWNLOAD_DONE when the target does not match the robot type', + action: { + type: 'robotUpdate:DOWNLOAD_DONE', + payload: 'ot2', + }, + initialState: { + ...INITIAL_STATE, + session: { + step: 'downloadFile', + stage: 'writing', + target: 'flex', + }, + }, + expected: { + ...INITIAL_STATE, + session: { + step: 'downloadFile', + stage: 'writing', + target: 'flex', + }, + }, + }, { name: 'handles robotUpdate:FILE_INFO', action: { diff --git a/app/src/redux/robot-update/constants.ts b/app/src/redux/robot-update/constants.ts index 2c0e625f892..cff9ed795ca 100644 --- a/app/src/redux/robot-update/constants.ts +++ b/app/src/redux/robot-update/constants.ts @@ -2,6 +2,7 @@ export const PREMIGRATION: 'premigration' = 'premigration' export const PREMIGRATION_RESTART: 'premigrationRestart' = 'premigrationRestart' +export const DOWNLOAD_FILE: 'downloadFile' = 'downloadFile' export const GET_TOKEN: 'getToken' = 'getToken' export const UPLOAD_FILE: 'uploadFile' = 'uploadFile' export const PROCESS_FILE: 'processFile' = 'processFile' @@ -32,6 +33,9 @@ export const REINSTALL: 'reinstall' = 'reinstall' // action types +export const ROBOTUPDATE_CHECKING_FOR_UPDATE: 'robotUpdate:CHECKING_FOR_UPDATE' = + 'robotUpdate:CHECKING_FOR_UPDATE' + export const ROBOTUPDATE_UPDATE_VERSION: 'robotUpdate:UPDATE_VERSION' = 'robotUpdate:UPDATE_VERSION' @@ -47,6 +51,9 @@ export const ROBOTUPDATE_DOWNLOAD_PROGRESS: 'robotUpdate:DOWNLOAD_PROGRESS' = export const ROBOTUPDATE_DOWNLOAD_ERROR: 'robotUpdate:DOWNLOAD_ERROR' = 'robotUpdate:DOWNLOAD_ERROR' +export const ROBOTUPDATE_DOWNLOAD_DONE: 'robotUpdate:DOWNLOAD_DONE' = + 'robotUpdate:DOWNLOAD_DONE' + export const ROBOTUPDATE_SET_UPDATE_SEEN: 'robotUpdate:SET_UPDATE_SEEN' = 'robotUpdate:SET_UPDATE_SEEN' diff --git a/app/src/redux/robot-update/epic.ts b/app/src/redux/robot-update/epic.ts index 07c22e03555..f3ec8fcfacf 100644 --- a/app/src/redux/robot-update/epic.ts +++ b/app/src/redux/robot-update/epic.ts @@ -62,6 +62,7 @@ import { ROBOTUPDATE_FILE_INFO, ROBOTUPDATE_CREATE_SESSION, ROBOTUPDATE_CREATE_SESSION_SUCCESS, + DOWNLOAD_FILE, } from './constants' import type { Observable } from 'rxjs' @@ -144,6 +145,19 @@ export const startUpdateEpic: Epic = (action$, state$) => }) ) +export const startUpdateAfterFileDownload: Epic = (_, state$) => { + return state$.pipe( + filter(passActiveSession({ step: DOWNLOAD_FILE, stage: DONE })), + switchMap(stateWithSession => { + const host: ViewableRobot = getRobotUpdateRobot(stateWithSession) as any + const robotModel = + host?.serverHealth?.robotModel === 'OT-3 Standard' ? 'flex' : 'ot2' + + return of(readSystemRobotUpdateFile(robotModel)) + }) + ) +} + // listen for a the active robot to come back with capabilities after premigration export const retryAfterPremigrationEpic: Epic = (_, state$) => { return state$.pipe( @@ -284,8 +298,6 @@ const passActiveSession = (props: Partial) => ( return ( robot !== null && !session?.error && - typeof session?.pathPrefix === 'string' && - typeof session?.token === 'string' && every( props, (value, key) => session?.[key as keyof RobotUpdateSession] === value @@ -449,6 +461,7 @@ export const removeMigratedRobotsEpic: Epic = (_, state$) => { export const robotUpdateEpic = combineEpics( startUpdateEpic, + startUpdateAfterFileDownload, retryAfterPremigrationEpic, startSessionAfterFileInfoEpic, createSessionEpic, diff --git a/app/src/redux/robot-update/hooks.ts b/app/src/redux/robot-update/hooks.ts index 840349ef331..9316e87ab35 100644 --- a/app/src/redux/robot-update/hooks.ts +++ b/app/src/redux/robot-update/hooks.ts @@ -7,6 +7,7 @@ type DispatchStartRobotUpdate = ( systemFile?: string | undefined ) => void +// Safely start a robot update. export function useDispatchStartRobotUpdate(): DispatchStartRobotUpdate { const dispatch = useDispatch<(a: Action) => void>() diff --git a/app/src/redux/robot-update/reducer.ts b/app/src/redux/robot-update/reducer.ts index fecebc4ee77..50c00c411e2 100644 --- a/app/src/redux/robot-update/reducer.ts +++ b/app/src/redux/robot-update/reducer.ts @@ -23,7 +23,7 @@ export const INITIAL_STATE: RobotUpdateState = { } export const initialSession = ( - robotName: string, + robotName: string | null, session: RobotUpdateSession | null ): RobotUpdateSession => ({ robotName, @@ -67,6 +67,21 @@ export const robotUpdateReducer: Reducer = ( } } + case Constants.ROBOTUPDATE_CHECKING_FOR_UPDATE: { + const session = state.session as RobotUpdateSession + const target = action.payload + + return { + ...state, + session: { + ...session, + step: Constants.DOWNLOAD_FILE, + stage: Constants.WRITING, + target, + }, + } + } + case Constants.ROBOTUPDATE_DOWNLOAD_PROGRESS: { return { ...state, @@ -79,6 +94,24 @@ export const robotUpdateReducer: Reducer = ( } } + case Constants.ROBOTUPDATE_DOWNLOAD_DONE: { + if (!state.session) return state + + const { target, ...session } = state.session + const isThisRobotDownloadDone = + session?.step === Constants.DOWNLOAD_FILE && target === action.payload + + return isThisRobotDownloadDone + ? { + ...state, + session: { + ...session, + stage: Constants.DONE, + }, + } + : state + } + case Constants.ROBOTUPDATE_DOWNLOAD_ERROR: { return { ...state, @@ -104,7 +137,12 @@ export const robotUpdateReducer: Reducer = ( return { ...state, - session: { ...session, step: Constants.GET_TOKEN }, + session: { + ...session, + robotName: host.name, + step: Constants.GET_TOKEN, + stage: null, + }, } } diff --git a/app/src/redux/robot-update/types.ts b/app/src/redux/robot-update/types.ts index c3fbe2193d0..81f43a7e571 100644 --- a/app/src/redux/robot-update/types.ts +++ b/app/src/redux/robot-update/types.ts @@ -39,6 +39,7 @@ export type UpdateSessionStage = export type UpdateSessionStep = | 'premigration' | 'premigrationRestart' + | 'downloadFile' | 'getToken' | 'uploadFile' | 'processFile' @@ -48,7 +49,7 @@ export type UpdateSessionStep = | 'finished' export interface RobotUpdateSession { - robotName: string + robotName: string | null fileInfo: RobotUpdateFileInfo | null token: string | null pathPrefix: string | null @@ -56,6 +57,7 @@ export interface RobotUpdateSession { stage: UpdateSessionStage | null progress: number | null error: string | null + target?: RobotUpdateTarget } export interface PerTargetRobotUpdateState { @@ -181,3 +183,5 @@ export type RobotUpdateAction = | { type: 'robotUpdate:CLEAR_SESSION' } | { type: 'robotUpdate:SET_UPDATE_SEEN'; meta: { robotName: string } } | { type: 'robotUpdate:FILE_UPLOAD_PROGRESS'; payload: number } + | { type: 'robotUpdate:CHECKING_FOR_UPDATE'; payload: RobotUpdateTarget } + | { type: 'robotUpdate:DOWNLOAD_DONE'; payload: RobotUpdateTarget } diff --git a/app/src/redux/sessions/__fixtures__/tip-length-calibration.ts b/app/src/redux/sessions/__fixtures__/tip-length-calibration.ts index 8877b9f1c69..fbb433c063b 100644 --- a/app/src/redux/sessions/__fixtures__/tip-length-calibration.ts +++ b/app/src/redux/sessions/__fixtures__/tip-length-calibration.ts @@ -35,6 +35,7 @@ export const mockTipLengthCalibrationSessionDetails: TipLengthCalibrationSession }, currentStep: 'labwareLoaded', labware: [mockTipLengthTipRack, mockTipLengthCalBlock], + supportedCommands: [], } export const mockTipLengthCalibrationSessionParams: TipLengthCalibrationSessionParams = { diff --git a/app/src/redux/sessions/tip-length-calibration/types.ts b/app/src/redux/sessions/tip-length-calibration/types.ts index 7e9980d6af7..16b5eebb865 100644 --- a/app/src/redux/sessions/tip-length-calibration/types.ts +++ b/app/src/redux/sessions/tip-length-calibration/types.ts @@ -8,7 +8,7 @@ import { TIP_LENGTH_STEP_MEASURING_TIP_OFFSET, TIP_LENGTH_STEP_CALIBRATION_COMPLETE, } from '../constants' -import type { CalibrationLabware } from '../types' +import type { CalibrationLabware, SessionCommandString } from '../types' import type { LabwareDefinition2, PipetteModel } from '@opentrons/shared-data' @@ -40,4 +40,5 @@ export interface TipLengthCalibrationSessionDetails { instrument: TipLengthCalibrationInstrument currentStep: TipLengthCalibrationStep labware: CalibrationLabware[] + supportedCommands: SessionCommandString[] } diff --git a/app/src/resources/deck_configuration/__tests__/hooks.test.ts b/app/src/resources/deck_configuration/__tests__/hooks.test.ts index e4818eb31d0..5a37005074e 100644 --- a/app/src/resources/deck_configuration/__tests__/hooks.test.ts +++ b/app/src/resources/deck_configuration/__tests__/hooks.test.ts @@ -1,26 +1,16 @@ import { when, resetAllWhenMocks } from 'jest-when' -import { v4 as uuidv4 } from 'uuid' import { useDeckConfigurationQuery } from '@opentrons/react-api-client' import { - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, } from '@opentrons/shared-data' -import { - CONFIGURED, - CONFLICTING, - NOT_CONFIGURED, - useLoadedFixturesConfigStatus, -} from '../hooks' - import type { UseQueryResult } from 'react-query' -import type { - DeckConfiguration, - LoadFixtureRunTimeCommand, -} from '@opentrons/shared-data' +import type { DeckConfiguration } from '@opentrons/shared-data' jest.mock('@opentrons/react-api-client') @@ -30,76 +20,40 @@ const mockUseDeckConfigurationQuery = useDeckConfigurationQuery as jest.MockedFu const MOCK_DECK_CONFIG: DeckConfiguration = [ { - fixtureLocation: 'A1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutA1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'B1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutB1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'C1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutC1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'D1', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutD1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, }, { - fixtureLocation: 'A3', - loadName: TRASH_BIN_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutA3', + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, }, { - fixtureLocation: 'B3', - loadName: STANDARD_SLOT_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutB3', + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, }, { - fixtureLocation: 'C3', - loadName: STAGING_AREA_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutC3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, }, { - fixtureLocation: 'D3', - loadName: WASTE_CHUTE_LOAD_NAME, - fixtureId: uuidv4(), + cutoutId: 'cutoutD3', + cutoutFixtureId: WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, }, ] -const WASTE_CHUTE_LOADED_FIXTURE: LoadFixtureRunTimeCommand = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: WASTE_CHUTE_LOAD_NAME, - location: { cutout: 'D3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', -} - -const STAGING_AREA_LOADED_FIXTURE: LoadFixtureRunTimeCommand = { - id: 'stubbed_load_fixture', - commandType: 'loadFixture', - params: { - fixtureId: 'stubbedFixtureId', - loadName: STAGING_AREA_LOAD_NAME, - location: { cutout: 'D3' }, - }, - createdAt: 'fakeTimestamp', - startedAt: 'fakeTimestamp', - completedAt: 'fakeTimestamp', - status: 'succeeded', -} - -describe('useLoadedFixturesConfigStatus', () => { +describe('useDeckConfigurationCompatibility', () => { beforeEach(() => { when(mockUseDeckConfigurationQuery) .calledWith() @@ -109,34 +63,5 @@ describe('useLoadedFixturesConfigStatus', () => { }) afterEach(() => resetAllWhenMocks()) - it('returns configured status if fixture is configured at location', () => { - const loadedFixturesConfigStatus = useLoadedFixturesConfigStatus([ - WASTE_CHUTE_LOADED_FIXTURE, - ]) - expect(loadedFixturesConfigStatus).toEqual([ - { ...WASTE_CHUTE_LOADED_FIXTURE, configurationStatus: CONFIGURED }, - ]) - }) - it('returns conflicted status if fixture is conflicted at location', () => { - const loadedFixturesConfigStatus = useLoadedFixturesConfigStatus([ - STAGING_AREA_LOADED_FIXTURE, - ]) - expect(loadedFixturesConfigStatus).toEqual([ - { ...STAGING_AREA_LOADED_FIXTURE, configurationStatus: CONFLICTING }, - ]) - }) - it('returns not configured status if fixture is not configured at location', () => { - when(mockUseDeckConfigurationQuery) - .calledWith() - .mockReturnValue({ - data: MOCK_DECK_CONFIG.slice(0, -1), - } as UseQueryResult) - - const loadedFixturesConfigStatus = useLoadedFixturesConfigStatus([ - WASTE_CHUTE_LOADED_FIXTURE, - ]) - expect(loadedFixturesConfigStatus).toEqual([ - { ...WASTE_CHUTE_LOADED_FIXTURE, configurationStatus: NOT_CONFIGURED }, - ]) - }) + it('returns configured status if fixture is configured at location', () => {}) }) diff --git a/app/src/resources/deck_configuration/__tests__/utils.test.ts b/app/src/resources/deck_configuration/__tests__/utils.test.ts new file mode 100644 index 00000000000..3fd7f03baac --- /dev/null +++ b/app/src/resources/deck_configuration/__tests__/utils.test.ts @@ -0,0 +1,142 @@ +import { RunTimeCommand } from '@opentrons/shared-data' +import { + FLEX_SIMPLEST_DECK_CONFIG, + getSimplestDeckConfigForProtocolCommands, +} from '../utils' + +const RUN_TIME_COMMAND_STUB_MIXIN: Pick< + RunTimeCommand, + 'id' | 'createdAt' | 'startedAt' | 'completedAt' | 'status' +> = { + id: 'fake_id', + createdAt: 'fake_createdAt', + startedAt: 'fake_startedAt', + completedAt: 'fake_createdAt', + status: 'succeeded', +} + +describe('getSimplestDeckConfigForProtocolCommands', () => { + it('returns simplest deck if no commands alter addressable areas', () => { + expect(getSimplestDeckConfigForProtocolCommands([])).toEqual( + FLEX_SIMPLEST_DECK_CONFIG + ) + }) + it('returns staging area fixtures if commands address column 4 areas', () => { + const cutoutConfigs = getSimplestDeckConfigForProtocolCommands([ + { + ...RUN_TIME_COMMAND_STUB_MIXIN, + commandType: 'loadLabware', + params: { + loadName: 'fake_load_name', + location: { slotName: 'A4' }, + version: 1, + namespace: 'fake_namespace', + }, + }, + { + ...RUN_TIME_COMMAND_STUB_MIXIN, + commandType: 'loadLabware', + params: { + loadName: 'fake_load_name', + location: { slotName: 'B4' }, + version: 1, + namespace: 'fake_namespace', + }, + }, + { + ...RUN_TIME_COMMAND_STUB_MIXIN, + commandType: 'loadLabware', + params: { + loadName: 'fake_load_name', + location: { slotName: 'C4' }, + version: 1, + namespace: 'fake_namespace', + }, + }, + { + ...RUN_TIME_COMMAND_STUB_MIXIN, + commandType: 'loadLabware', + params: { + loadName: 'fake_load_name', + location: { slotName: 'D4' }, + version: 1, + namespace: 'fake_namespace', + }, + }, + ]) + expect(cutoutConfigs).toEqual([ + ...FLEX_SIMPLEST_DECK_CONFIG.slice(0, 8), + { + cutoutId: 'cutoutA3', + cutoutFixtureId: 'stagingAreaRightSlot', + requiredAddressableAreas: ['A4'], + }, + { + cutoutId: 'cutoutB3', + cutoutFixtureId: 'stagingAreaRightSlot', + requiredAddressableAreas: ['B4'], + }, + { + cutoutId: 'cutoutC3', + cutoutFixtureId: 'stagingAreaRightSlot', + requiredAddressableAreas: ['C4'], + }, + { + cutoutId: 'cutoutD3', + cutoutFixtureId: 'stagingAreaRightSlot', + requiredAddressableAreas: ['D4'], + }, + ]) + }) + it('returns simplest cutout fixture where many are possible', () => { + const cutoutConfigs = getSimplestDeckConfigForProtocolCommands([ + { + ...RUN_TIME_COMMAND_STUB_MIXIN, + commandType: 'moveLabware', + params: { + newLocation: { addressableAreaName: 'gripperWasteChute' }, + labwareId: 'fake_labwareId', + strategy: 'usingGripper', + }, + }, + ]) + expect(cutoutConfigs).toEqual([ + ...FLEX_SIMPLEST_DECK_CONFIG.slice(0, 11), + { + cutoutId: 'cutoutD3', + cutoutFixtureId: 'wasteChuteRightAdapterNoCover', + requiredAddressableAreas: ['gripperWasteChute'], + }, + ]) + }) + it('returns compatible cutout fixture where multiple addressable requirements present', () => { + const cutoutConfigs = getSimplestDeckConfigForProtocolCommands([ + { + ...RUN_TIME_COMMAND_STUB_MIXIN, + commandType: 'moveLabware', + params: { + newLocation: { addressableAreaName: 'gripperWasteChute' }, + labwareId: 'fake_labwareId', + strategy: 'usingGripper', + }, + }, + { + ...RUN_TIME_COMMAND_STUB_MIXIN, + commandType: 'moveLabware', + params: { + newLocation: { addressableAreaName: 'D4' }, + labwareId: 'fake_labwareId', + strategy: 'usingGripper', + }, + }, + ]) + expect(cutoutConfigs).toEqual([ + ...FLEX_SIMPLEST_DECK_CONFIG.slice(0, 11), + { + cutoutId: 'cutoutD3', + cutoutFixtureId: 'stagingAreaSlotWithWasteChuteRightAdapterNoCover', + requiredAddressableAreas: ['gripperWasteChute', 'D4'], + }, + ]) + }) +}) diff --git a/app/src/resources/deck_configuration/hooks.ts b/app/src/resources/deck_configuration/hooks.ts index 967d46b5119..251dda3b4c8 100644 --- a/app/src/resources/deck_configuration/hooks.ts +++ b/app/src/resources/deck_configuration/hooks.ts @@ -1,48 +1,60 @@ +import { parseAllAddressableAreas } from '@opentrons/api-client' import { useDeckConfigurationQuery } from '@opentrons/react-api-client' -import { STANDARD_SLOT_LOAD_NAME } from '@opentrons/shared-data' - -import type { Fixture, LoadFixtureRunTimeCommand } from '@opentrons/shared-data' - -export const CONFIGURED = 'configured' -export const CONFLICTING = 'conflicting' -export const NOT_CONFIGURED = 'not configured' - -type LoadedFixtureConfigurationStatus = - | typeof CONFIGURED - | typeof CONFLICTING - | typeof NOT_CONFIGURED - -type LoadedFixtureConfiguration = LoadFixtureRunTimeCommand & { - configurationStatus: LoadedFixtureConfigurationStatus +import { + FLEX_ROBOT_TYPE, + getDeckDefFromRobotType, +} from '@opentrons/shared-data' + +import { + getCutoutFixturesForCutoutId, + getCutoutIdForAddressableArea, +} from './utils' + +import type { + CutoutFixtureId, + RobotType, + RunTimeCommand, +} from '@opentrons/shared-data' +import type { CutoutConfigProtocolSpec } from './utils' + +export interface CutoutConfigAndCompatibility extends CutoutConfigProtocolSpec { + compatibleCutoutFixtureIds: CutoutFixtureId[] } - -export function useLoadedFixturesConfigStatus( - loadedFixtures: LoadFixtureRunTimeCommand[] -): LoadedFixtureConfiguration[] { +export function useDeckConfigurationCompatibility( + robotType: RobotType, + protocolCommands: RunTimeCommand[] +): CutoutConfigAndCompatibility[] { const deckConfig = useDeckConfigurationQuery().data ?? [] - - return loadedFixtures.map(loadedFixture => { - const deckConfigurationAtLocation = deckConfig.find( - (deckFixture: Fixture) => - deckFixture.fixtureLocation === loadedFixture.params.location.cutout - ) - - let configurationStatus: LoadedFixtureConfigurationStatus = NOT_CONFIGURED - if ( - deckConfigurationAtLocation != null && - deckConfigurationAtLocation.loadName === loadedFixture.params.loadName - ) { - configurationStatus = CONFIGURED - // special casing this for now until we know what the backend will give us. It is only - // conflicting if the current deck configuration fixture is not the desired or standard slot - } else if ( - deckConfigurationAtLocation != null && - deckConfigurationAtLocation.loadName !== loadedFixture.params.loadName && - deckConfigurationAtLocation.loadName !== STANDARD_SLOT_LOAD_NAME - ) { - configurationStatus = CONFLICTING - } - - return { ...loadedFixture, configurationStatus } - }) + if (robotType !== FLEX_ROBOT_TYPE) return [] + const deckDef = getDeckDefFromRobotType(robotType) + const allAddressableAreas = parseAllAddressableAreas(protocolCommands) + return deckConfig.reduce( + (acc, { cutoutId, cutoutFixtureId }) => { + const fixturesThatMountToCutoutId = getCutoutFixturesForCutoutId( + cutoutId, + deckDef.cutoutFixtures + ) + const requiredAddressableAreasForCutoutId = allAddressableAreas.filter( + aa => + getCutoutIdForAddressableArea(aa, fixturesThatMountToCutoutId) === + cutoutId + ) + return [ + ...acc, + { + cutoutId, + cutoutFixtureId: cutoutFixtureId, + requiredAddressableAreas: requiredAddressableAreasForCutoutId, + compatibleCutoutFixtureIds: fixturesThatMountToCutoutId + .filter(cf => + requiredAddressableAreasForCutoutId.every(aa => + cf.providesAddressableAreas[cutoutId].includes(aa) + ) + ) + .map(cf => cf.id), + }, + ] + }, + [] + ) } diff --git a/app/src/resources/deck_configuration/types.ts b/app/src/resources/deck_configuration/types.ts new file mode 100644 index 00000000000..2929de72deb --- /dev/null +++ b/app/src/resources/deck_configuration/types.ts @@ -0,0 +1,11 @@ +import type { + CutoutId, + CutoutFixtureId, + AddressableAreaName, +} from '@opentrons/shared-data' + +export interface CutoutConfig { + cutoutId: CutoutId + cutoutFixtureId: CutoutFixtureId + requiredAddressableAreas: AddressableAreaName[] +} diff --git a/app/src/resources/deck_configuration/utils.ts b/app/src/resources/deck_configuration/utils.ts index 7acdd5c80ef..4393ccfbadb 100644 --- a/app/src/resources/deck_configuration/utils.ts +++ b/app/src/resources/deck_configuration/utils.ts @@ -1,23 +1,245 @@ -import { v4 as uuidv4 } from 'uuid' +import { parseAllAddressableAreas } from '@opentrons/api-client' +import { + FLEX_ROBOT_TYPE, + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS, + getAddressableAreaFromSlotId, + getDeckDefFromRobotType, +} from '@opentrons/shared-data' -import { parseInitialLoadedFixturesByCutout } from '@opentrons/api-client' -import { STANDARD_SLOT_DECK_CONFIG_FIXTURE } from '@opentrons/components' +import type { + CutoutConfig, + CutoutId, + RunTimeCommand, + CutoutFixture, + AddressableAreaName, + DeckDefinition, +} from '@opentrons/shared-data' -import type { DeckConfiguration, RunTimeCommand } from '@opentrons/shared-data' +export interface CutoutConfigProtocolSpec extends CutoutConfig { + requiredAddressableAreas: AddressableAreaName[] +} + +export const FLEX_SIMPLEST_DECK_CONFIG: CutoutConfigProtocolSpec[] = [ + { + cutoutId: 'cutoutA1', + cutoutFixtureId: 'singleLeftSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutB1', + cutoutFixtureId: 'singleLeftSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutC1', + cutoutFixtureId: 'singleLeftSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutD1', + cutoutFixtureId: 'singleLeftSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutA2', + cutoutFixtureId: 'singleCenterSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutB2', + cutoutFixtureId: 'singleCenterSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutC2', + cutoutFixtureId: 'singleCenterSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutD2', + cutoutFixtureId: 'singleCenterSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutA3', + cutoutFixtureId: 'singleRightSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutB3', + cutoutFixtureId: 'singleRightSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutC3', + cutoutFixtureId: 'singleRightSlot', + requiredAddressableAreas: [], + }, + { + cutoutId: 'cutoutD3', + cutoutFixtureId: 'singleRightSlot', + requiredAddressableAreas: [], + }, +] -export function getDeckConfigFromProtocolCommands( +export function getSimplestDeckConfigForProtocolCommands( protocolAnalysisCommands: RunTimeCommand[] -): DeckConfiguration { - const loadedFixtureCommands = Object.values( - parseInitialLoadedFixturesByCutout(protocolAnalysisCommands) +): CutoutConfigProtocolSpec[] { + // TODO(BC, 2023-11-06): abstract out the robot type + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) + + const addressableAreas = parseAllAddressableAreas(protocolAnalysisCommands) + const simplestDeckConfig = addressableAreas.reduce< + CutoutConfigProtocolSpec[] + >((acc, addressableArea) => { + const cutoutFixturesForAddressableArea = getCutoutFixturesForAddressableAreas( + [addressableArea], + deckDef.cutoutFixtures + ) + const cutoutIdForAddressableArea = getCutoutIdForAddressableArea( + addressableArea, + cutoutFixturesForAddressableArea + ) + const cutoutFixturesForCutoutId = + cutoutIdForAddressableArea != null + ? getCutoutFixturesForCutoutId( + cutoutIdForAddressableArea, + deckDef.cutoutFixtures + ) + : null + + const existingCutoutConfig = acc.find( + cutoutConfig => cutoutConfig.cutoutId === cutoutIdForAddressableArea + ) + + if ( + existingCutoutConfig != null && + cutoutFixturesForCutoutId != null && + cutoutIdForAddressableArea != null + ) { + const indexOfExistingFixture = cutoutFixturesForCutoutId.findIndex( + ({ id }) => id === existingCutoutConfig.cutoutFixtureId + ) + const accIndex = acc.findIndex( + ({ cutoutId }) => cutoutId === cutoutIdForAddressableArea + ) + const previousRequiredAAs = acc[accIndex]?.requiredAddressableAreas + const allNextRequiredAddressableAreas = previousRequiredAAs.includes( + addressableArea + ) + ? previousRequiredAAs + : [...previousRequiredAAs, addressableArea] + const nextCompatibleCutoutFixture = getSimplestFixtureForAddressableAreas( + cutoutIdForAddressableArea, + allNextRequiredAddressableAreas, + cutoutFixturesForCutoutId + ) + const indexOfCurrentFixture = cutoutFixturesForCutoutId.findIndex( + ({ id }) => id === nextCompatibleCutoutFixture?.id + ) + + if ( + nextCompatibleCutoutFixture != null && + indexOfCurrentFixture > indexOfExistingFixture + ) { + return [ + ...acc.slice(0, accIndex), + { + cutoutId: cutoutIdForAddressableArea, + cutoutFixtureId: nextCompatibleCutoutFixture.id, + requiredAddressableAreas: allNextRequiredAddressableAreas, + }, + ...acc.slice(accIndex + 1), + ] + } + } + return acc + }, FLEX_SIMPLEST_DECK_CONFIG) + + return simplestDeckConfig +} + +export function getCutoutFixturesForAddressableAreas( + addressableAreas: AddressableAreaName[], + cutoutFixtures: CutoutFixture[] +): CutoutFixture[] { + return cutoutFixtures.filter(cutoutFixture => + Object.values(cutoutFixture.providesAddressableAreas).some(providedAAs => + addressableAreas.every(aa => providedAAs.includes(aa)) + ) + ) +} + +export function getCutoutFixturesForCutoutId( + cutoutId: CutoutId, + cutoutFixtures: CutoutFixture[] +): CutoutFixture[] { + return cutoutFixtures.filter(cutoutFixture => + cutoutFixture.mayMountTo.some(mayMountTo => mayMountTo.includes(cutoutId)) ) +} + +export function getCutoutIdForSlotName( + slotName: string, + deckDef: DeckDefinition +): CutoutId | null { + const addressableArea = getAddressableAreaFromSlotId(slotName, deckDef) + const cutoutIdForSlotName = + addressableArea != null + ? getCutoutIdForAddressableArea( + addressableArea.id, + deckDef.cutoutFixtures + ) + : null + + return cutoutIdForSlotName +} + +export function getCutoutIdForAddressableArea( + addressableArea: AddressableAreaName, + cutoutFixtures: CutoutFixture[] +): CutoutId | null { + return cutoutFixtures.reduce((acc, cutoutFixture) => { + const [cutoutId] = + Object.entries( + cutoutFixture.providesAddressableAreas + ).find(([_cutoutId, providedAAs]) => + providedAAs.includes(addressableArea) + ) ?? [] + return (cutoutId as CutoutId) ?? acc + }, null) +} - const deckConfig = loadedFixtureCommands.map(command => ({ - fixtureId: command.params.fixtureId ?? uuidv4(), - fixtureLocation: command.params.location.cutout, - loadName: command.params.loadName, - })) +export function getSimplestFixtureForAddressableAreas( + cutoutId: CutoutId, + requiredAddressableAreas: AddressableAreaName[], + allCutoutFixtures: CutoutFixture[] +): CutoutFixture | null { + const cutoutFixturesForCutoutId = getCutoutFixturesForCutoutId( + cutoutId, + allCutoutFixtures + ) + const nextCompatibleCutoutFixtures = getCutoutFixturesForAddressableAreas( + requiredAddressableAreas, + cutoutFixturesForCutoutId + ) + return nextCompatibleCutoutFixtures?.[0] ?? null +} + +export function getRequiredDeckConfig( + deckConfigProtocolSpec: T[] +): T[] { + const nonSingleSlotDeckConfigCompatibility = deckConfigProtocolSpec.filter( + ({ requiredAddressableAreas }) => + // required AA list includes a non-single-slot AA + !requiredAddressableAreas.every(aa => + FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS.includes(aa) + ) + ) + // fixture includes at least 1 required AA + const requiredDeckConfigProtocolSpec = nonSingleSlotDeckConfigCompatibility.filter( + fixture => fixture.requiredAddressableAreas.length > 0 + ) - // TODO(bh, 2023-10-18): remove stub when load fixture commands available - return deckConfig.length > 0 ? deckConfig : STANDARD_SLOT_DECK_CONFIG_FIXTURE + return requiredDeckConfigProtocolSpec } diff --git a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx index 1fe12ccdb91..bc8f719828b 100644 --- a/components/src/hardware-sim/BaseDeck/BaseDeck.tsx +++ b/components/src/hardware-sim/BaseDeck/BaseDeck.tsx @@ -1,46 +1,49 @@ import * as React from 'react' import { - RobotType, getDeckDefFromRobotType, - ModuleModel, - ModuleLocation, getModuleDef2, - LabwareDefinition2, + getPositionFromSlotId, inferModuleOrientationFromXCoordinate, - LabwareLocation, OT2_ROBOT_TYPE, - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + SINGLE_SLOT_FIXTURES, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_CUTOUT, + WASTE_CHUTE_ONLY_FIXTURES, + WASTE_CHUTE_STAGING_AREA_FIXTURES, } from '@opentrons/shared-data' + import { RobotCoordinateSpace } from '../RobotCoordinateSpace' import { Module } from '../Module' import { LabwareRender } from '../Labware' import { FlexTrash } from '../Deck/FlexTrash' -import { DeckFromData } from '../Deck/DeckFromData' +import { DeckFromLayers } from '../Deck/DeckFromLayers' import { SlotLabels } from '../Deck' import { COLORS } from '../../ui-style-constants' -import { - // EXTENDED_DECK_CONFIG_FIXTURE, - STANDARD_SLOT_DECK_CONFIG_FIXTURE, -} from './__fixtures__' +import { Svg } from '../../primitives' import { SingleSlotFixture } from './SingleSlotFixture' import { StagingAreaFixture } from './StagingAreaFixture' import { WasteChuteFixture } from './WasteChuteFixture' -// import { WasteChuteStagingAreaFixture } from './WasteChuteStagingAreaFixture' +import { WasteChuteStagingAreaFixture } from './WasteChuteStagingAreaFixture' -import type { DeckConfiguration } from '@opentrons/shared-data' -import type { TrashLocation } from '../Deck/FlexTrash' +import type { + DeckConfiguration, + LabwareDefinition2, + LabwareLocation, + ModuleLocation, + ModuleModel, + RobotType, +} from '@opentrons/shared-data' +import type { TrashCutoutId } from '../Deck/FlexTrash' import type { StagingAreaLocation } from './StagingAreaFixture' -import type { WasteChuteLocation } from './WasteChuteFixture' import type { WellFill } from '../Labware' interface BaseDeckProps { + deckConfig: DeckConfiguration robotType: RobotType - labwareLocations: Array<{ + labwareLocations?: Array<{ labwareLocation: LabwareLocation definition: LabwareDefinition2 wellFill?: WellFill @@ -48,7 +51,7 @@ interface BaseDeckProps { labwareChildren?: React.ReactNode onLabwareClick?: () => void }> - moduleLocations: Array<{ + moduleLocations?: Array<{ moduleModel: ModuleModel moduleLocation: ModuleLocation nestedLabwareDef?: LabwareDefinition2 | null @@ -58,57 +61,86 @@ interface BaseDeckProps { moduleChildren?: React.ReactNode onLabwareClick?: () => void }> - deckConfig?: DeckConfiguration deckLayerBlocklist?: string[] showExpansion?: boolean lightFill?: string darkFill?: string children?: React.ReactNode showSlotLabels?: boolean + /** whether to make wrapping svg tag animatable via @react-spring/web, defaults to false */ + animatedSVG?: boolean + /** extra props to pass to svg tag */ + svgProps?: React.ComponentProps } export function BaseDeck(props: BaseDeckProps): JSX.Element { const { robotType, - moduleLocations, - labwareLocations, + moduleLocations = [], + labwareLocations = [], lightFill = COLORS.light1, darkFill = COLORS.darkGreyEnabled, deckLayerBlocklist = [], - // TODO(bh, 2023-10-09): remove deck config fixture for Flex after migration to v4 - // deckConfig = EXTENDED_DECK_CONFIG_FIXTURE, - deckConfig = STANDARD_SLOT_DECK_CONFIG_FIXTURE, + deckConfig, showExpansion = true, children, - showSlotLabels = false, + showSlotLabels = true, + animatedSVG = false, + svgProps = {}, } = props const deckDef = getDeckDefFromRobotType(robotType) const singleSlotFixtures = deckConfig.filter( - fixture => fixture.loadName === STANDARD_SLOT_LOAD_NAME + fixture => + fixture.cutoutFixtureId != null && + SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) ) const stagingAreaFixtures = deckConfig.filter( - fixture => fixture.loadName === STAGING_AREA_LOAD_NAME + fixture => fixture.cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE ) const trashBinFixtures = deckConfig.filter( - fixture => fixture.loadName === TRASH_BIN_LOAD_NAME + fixture => fixture.cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE + ) + const wasteChuteOnlyFixtures = deckConfig.filter( + fixture => + fixture.cutoutFixtureId != null && + WASTE_CHUTE_ONLY_FIXTURES.includes(fixture.cutoutFixtureId) && + fixture.cutoutId === WASTE_CHUTE_CUTOUT ) - const wasteChuteFixtures = deckConfig.filter( - fixture => fixture.loadName === WASTE_CHUTE_LOAD_NAME + const wasteChuteStagingAreaFixtures = deckConfig.filter( + fixture => + fixture.cutoutFixtureId != null && + WASTE_CHUTE_STAGING_AREA_FIXTURES.includes(fixture.cutoutFixtureId) && + fixture.cutoutId === WASTE_CHUTE_CUTOUT ) return ( {robotType === OT2_ROBOT_TYPE ? ( - + ) : ( <> + {showSlotLabels ? ( + 0 || + wasteChuteStagingAreaFixtures.length > 0 + } + /> + ) : null} {singleSlotFixtures.map(fixture => ( ( ))} {trashBinFixtures.map(fixture => ( - + ))} - {wasteChuteFixtures.map(fixture => ( + {wasteChuteOnlyFixtures.map(fixture => ( + ))} + {wasteChuteStagingAreaFixtures.map(fixture => ( + )} - {moduleLocations.map( - ({ - moduleModel, - moduleLocation, - nestedLabwareDef, - nestedLabwareWellFill, - innerProps, - moduleChildren, - onLabwareClick, - }) => { - const slotDef = deckDef.locations.orderedSlots.find( - s => s.id === moduleLocation.slotName - ) - const moduleDef = getModuleDef2(moduleModel) - return slotDef != null ? ( - - {nestedLabwareDef != null ? ( + <> + {moduleLocations.map( + ({ + moduleModel, + moduleLocation, + nestedLabwareDef, + nestedLabwareWellFill, + innerProps, + moduleChildren, + onLabwareClick, + }) => { + const slotPosition = getPositionFromSlotId( + moduleLocation.slotName, + deckDef + ) + + const moduleDef = getModuleDef2(moduleModel) + return slotPosition != null ? ( + + {nestedLabwareDef != null ? ( + + ) : null} + {moduleChildren} + + ) : null + } + )} + {labwareLocations.map( + ({ + labwareLocation, + definition, + labwareChildren, + wellFill, + onLabwareClick, + }) => { + if ( + labwareLocation === 'offDeck' || + !('slotName' in labwareLocation) + ) { + return null + } + + const slotPosition = getPositionFromSlotId( + labwareLocation.slotName, + deckDef + ) + + return slotPosition != null ? ( + - ) : null} - {moduleChildren} - - ) : null - } - )} - {labwareLocations.map( - ({ - labwareLocation, - definition, - labwareChildren, - wellFill, - onLabwareClick, - }) => { - const slotDef = deckDef.locations.orderedSlots.find( - s => - labwareLocation !== 'offDeck' && - 'slotName' in labwareLocation && - s.id === labwareLocation.slotName - ) - return slotDef != null ? ( - - - {labwareChildren} - - ) : null - } - )} - {showSlotLabels ? ( - - ) : null} + {labwareChildren} + + ) : null + } + )} + {children} ) diff --git a/components/src/hardware-sim/BaseDeck/SingleSlotFixture.tsx b/components/src/hardware-sim/BaseDeck/SingleSlotFixture.tsx index 7e763a0fb7f..0fe3ff526e6 100644 --- a/components/src/hardware-sim/BaseDeck/SingleSlotFixture.tsx +++ b/components/src/hardware-sim/BaseDeck/SingleSlotFixture.tsx @@ -3,10 +3,14 @@ import * as React from 'react' import { SlotBase } from './SlotBase' import { SlotClip } from './SlotClip' -import type { Cutout, DeckDefinition, ModuleType } from '@opentrons/shared-data' +import type { + CutoutId, + DeckDefinition, + ModuleType, +} from '@opentrons/shared-data' interface SingleSlotFixtureProps extends React.SVGProps { - cutoutLocation: Cutout + cutoutId: CutoutId deckDefinition: DeckDefinition moduleType?: ModuleType fixtureBaseColor?: React.SVGProps['fill'] @@ -18,7 +22,7 @@ export function SingleSlotFixture( props: SingleSlotFixtureProps ): JSX.Element | null { const { - cutoutLocation, + cutoutId, deckDefinition, fixtureBaseColor, slotClipColor, @@ -26,9 +30,8 @@ export function SingleSlotFixture( ...restProps } = props - // TODO(bh, 2023-10-09): migrate from "orderedSlots" to v4 "cutouts" key - const cutoutDef = deckDefinition?.locations.orderedSlots.find( - s => s.id === cutoutLocation + const cutoutDef = deckDefinition?.locations.cutouts.find( + s => s.id === cutoutId ) if (cutoutDef == null) { console.warn( @@ -38,9 +41,9 @@ export function SingleSlotFixture( } const contentsByCutoutLocation: { - [cutoutLocation in Cutout]: JSX.Element + [cutoutId in CutoutId]: JSX.Element } = { - A1: ( + cutoutA1: ( <> {showExpansion ? ( ), - A2: ( + cutoutA2: ( <> ), - A3: ( + cutoutA3: ( <> ), - B1: ( + cutoutB1: ( <> ), - B2: ( + cutoutB2: ( <> ), - B3: ( + cutoutB3: ( <> ), - C1: ( + cutoutC1: ( <> ), - C2: ( + cutoutC2: ( <> ), - C3: ( + cutoutC3: ( <> ), - D1: ( + cutoutD1: ( <> ), - D2: ( + cutoutD2: ( <> ), - D3: ( + cutoutD3: ( <> {contentsByCutoutLocation[cutoutLocation]} + return {contentsByCutoutLocation[cutoutId]} } diff --git a/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx b/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx index 2b3d8491da6..107da94b8c2 100644 --- a/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx +++ b/components/src/hardware-sim/BaseDeck/StagingAreaFixture.tsx @@ -5,10 +5,14 @@ import { SlotClip } from './SlotClip' import type { DeckDefinition, ModuleType } from '@opentrons/shared-data' -export type StagingAreaLocation = 'A3' | 'B3' | 'C3' | 'D3' +export type StagingAreaLocation = + | 'cutoutA3' + | 'cutoutB3' + | 'cutoutC3' + | 'cutoutD3' interface StagingAreaFixtureProps extends React.SVGProps { - cutoutLocation: StagingAreaLocation + cutoutId: StagingAreaLocation deckDefinition: DeckDefinition moduleType?: ModuleType fixtureBaseColor?: React.SVGProps['fill'] @@ -20,16 +24,15 @@ export function StagingAreaFixture( props: StagingAreaFixtureProps ): JSX.Element | null { const { - cutoutLocation, + cutoutId, deckDefinition, fixtureBaseColor, slotClipColor, ...restProps } = props - // TODO(bh, 2023-10-09): migrate from "orderedSlots" to v4 "cutouts" key - const cutoutDef = deckDefinition?.locations.orderedSlots.find( - s => s.id === cutoutLocation + const cutoutDef = deckDefinition?.locations.cutouts.find( + s => s.id === cutoutId ) if (cutoutDef == null) { console.warn( @@ -38,11 +41,10 @@ export function StagingAreaFixture( return null } - // TODO(bh, 2023-10-10): adjust base and clip d values if needed to fit v4 deck definition const contentsByCutoutLocation: { - [cutoutLocation in StagingAreaLocation]: JSX.Element + [cutoutId in StagingAreaLocation]: JSX.Element } = { - A3: ( + cutoutA3: ( <> ), - B3: ( + cutoutB3: ( <> ), - C3: ( + cutoutC3: ( <> ), - D3: ( + cutoutD3: ( <> {contentsByCutoutLocation[cutoutLocation]} + return {contentsByCutoutLocation[cutoutId]} } diff --git a/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx b/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx index a4ac6673bf5..9f562731b72 100644 --- a/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx +++ b/components/src/hardware-sim/BaseDeck/WasteChuteFixture.tsx @@ -1,19 +1,23 @@ import * as React from 'react' +import { WASTE_CHUTE_CUTOUT } from '@opentrons/shared-data' + import { Icon } from '../../icons' import { Flex, Text } from '../../primitives' -import { ALIGN_CENTER, DIRECTION_COLUMN, JUSTIFY_CENTER } from '../../styles' -import { BORDERS, COLORS, TYPOGRAPHY } from '../../ui-style-constants' +import { + ALIGN_CENTER, + DIRECTION_COLUMN, + JUSTIFY_CENTER, + TEXT_ALIGN_CENTER, +} from '../../styles' +import { COLORS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' import { SlotBase } from './SlotBase' import type { DeckDefinition, ModuleType } from '@opentrons/shared-data' -// waste chute only in cutout location D3 -export type WasteChuteLocation = 'D3' - interface WasteChuteFixtureProps extends React.SVGProps { - cutoutLocation: WasteChuteLocation + cutoutId: typeof WASTE_CHUTE_CUTOUT deckDefinition: DeckDefinition moduleType?: ModuleType fixtureBaseColor?: React.SVGProps['fill'] @@ -25,23 +29,22 @@ export function WasteChuteFixture( props: WasteChuteFixtureProps ): JSX.Element | null { const { - cutoutLocation, + cutoutId, deckDefinition, fixtureBaseColor = COLORS.light1, slotClipColor = COLORS.darkGreyEnabled, ...restProps } = props - if (cutoutLocation !== 'D3') { + if (cutoutId !== 'cutoutD3') { console.warn( - `cannot render WasteChuteFixture in given cutout location ${cutoutLocation}` + `cannot render WasteChuteFixture in given cutout location ${cutoutId}` ) return null } - // TODO(bh, 2023-10-09): migrate from "orderedSlots" to v4 "cutouts" key - const cutoutDef = deckDefinition?.locations.orderedSlots.find( - s => s.id === cutoutLocation + const cutoutDef = deckDefinition?.locations.cutouts.find( + s => s.id === cutoutId ) if (cutoutDef == null) { console.warn( @@ -50,7 +53,6 @@ export function WasteChuteFixture( return null } - // TODO(bh, 2023-10-10): adjust base and clip d values if needed to fit v4 deck definition return ( - - Waste chute + + + Waste chute + ) diff --git a/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx b/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx index b2cdbad8e2d..564db96c5fb 100644 --- a/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx +++ b/components/src/hardware-sim/BaseDeck/WasteChuteStagingAreaFixture.tsx @@ -1,16 +1,17 @@ import * as React from 'react' +import { WASTE_CHUTE_CUTOUT } from '@opentrons/shared-data' + import { COLORS } from '../../ui-style-constants' import { SlotBase } from './SlotBase' import { SlotClip } from './SlotClip' import { WasteChute } from './WasteChuteFixture' import type { DeckDefinition, ModuleType } from '@opentrons/shared-data' -import type { WasteChuteLocation } from './WasteChuteFixture' interface WasteChuteStagingAreaFixtureProps extends React.SVGProps { - cutoutLocation: WasteChuteLocation + cutoutId: typeof WASTE_CHUTE_CUTOUT deckDefinition: DeckDefinition moduleType?: ModuleType fixtureBaseColor?: React.SVGProps['fill'] @@ -22,23 +23,22 @@ export function WasteChuteStagingAreaFixture( props: WasteChuteStagingAreaFixtureProps ): JSX.Element | null { const { - cutoutLocation, + cutoutId, deckDefinition, fixtureBaseColor = COLORS.light1, slotClipColor = COLORS.darkGreyEnabled, ...restProps } = props - if (cutoutLocation !== 'D3') { + if (cutoutId !== WASTE_CHUTE_CUTOUT) { console.warn( - `cannot render WasteChuteStagingAreaFixture in given cutout location ${cutoutLocation}` + `cannot render WasteChuteStagingAreaFixture in given cutout location ${cutoutId}` ) return null } - // TODO(bh, 2023-10-09): migrate from "orderedSlots" to v4 "cutouts" key - const cutoutDef = deckDefinition?.locations.orderedSlots.find( - s => s.id === cutoutLocation + const cutoutDef = deckDefinition?.locations.cutouts.find( + s => s.id === cutoutId ) if (cutoutDef == null) { console.warn( @@ -47,9 +47,7 @@ export function WasteChuteStagingAreaFixture( return null } - // TODO(bh, 2023-10-10): adjust base and clip d values if needed to fit v4 deck definition return ( - // TODO: render a "Waste chute" foreign object similar to FlexTrash { // be sure we don't try to render for an OT-2 - if (robotType !== 'OT-3 Standard') return null + if (robotType !== FLEX_ROBOT_TYPE) return null + + const deckDefinition = getDeckDefFromRobotType(robotType) - const deckDef = getDeckDefFromRobotType(robotType) - // TODO(bh, 2023-10-09): migrate from "orderedSlots" to v4 "cutouts" key - const trashSlot = deckDef.locations.orderedSlots.find( - slot => slot.id === trashLocation + const trashCutout = deckDefinition.locations.cutouts.find( + cutout => cutout.id === trashCutoutId ) - // retrieve slot x,y positions and dimensions from deck definition for the given trash slot - // TODO(bh, 2023-10-09): refactor position, offsets, and rotation after v4 migration - const [x = 0, y = 0] = trashSlot?.position ?? [] - const { xDimension: slotXDimension = 0, yDimension: slotYDimension = 0 } = - trashSlot?.boundingBox ?? {} + // retrieve slot x,y positions and dimensions from deck definition for the given trash cutout location + const [x = 0, y = 0] = trashCutout?.position ?? [] + + // a standard addressable area slot bounding box dimension + const { + xDimension: slotXDimension = 0, + yDimension: slotYDimension = 0, + } = deckDefinition.locations.addressableAreas[0].boundingBox // adjust for dimensions from trash definition const { x: xAdjustment, y: yAdjustment } = trashDef.cornerOffsetFromSlot @@ -60,10 +67,10 @@ export const FlexTrash = ({ // rotate trash 180 degrees in column 1 const rotateDegrees = - trashLocation === 'A1' || - trashLocation === 'B1' || - trashLocation === 'C1' || - trashLocation === 'D1' + trashCutoutId === 'cutoutA1' || + trashCutoutId === 'cutoutB1' || + trashCutoutId === 'cutoutC1' || + trashCutoutId === 'cutoutD1' ? '180' : '0' @@ -86,11 +93,12 @@ export const FlexTrash = ({ backgroundColor={backgroundColor} borderRadius={BORDERS.radiusSoftCorners} justifyContent={JUSTIFY_CENTER} + gridGap={SPACING.spacing8} width="100%" > {rotateDegrees === '180' ? ( {rotateDegrees === '0' ? ( - + Trash bin ) : null} diff --git a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.stories.tsx b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.stories.tsx index ca42a50ebe7..89ddd9fcdb2 100644 --- a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.stories.tsx +++ b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.stories.tsx @@ -1,8 +1,17 @@ import * as React from 'react' import fixture_96_plate from '@opentrons/shared-data/labware/fixtures/2/fixture_96_plate.json' -import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import { + FLEX_ROBOT_TYPE, + SINGLE_CENTER_SLOT_FIXTURE, + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, + STAGING_AREA_RIGHT_SLOT_FIXTURE, +} from '@opentrons/shared-data' import { MoveLabwareOnDeck as MoveLabwareOnDeckComponent } from './MoveLabwareOnDeck' -import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { + DeckConfiguration, + LabwareDefinition2, +} from '@opentrons/shared-data' import type { Meta, StoryObj } from '@storybook/react' @@ -14,16 +23,70 @@ const meta: Meta> = { export default meta type Story = StoryObj> +const FLEX_SIMPLEST_DECK_CONFIG: DeckConfiguration = [ + { + cutoutId: 'cutoutA1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutB1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutC1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutD1', + cutoutFixtureId: SINGLE_LEFT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutA2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutB2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutC2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutD2', + cutoutFixtureId: SINGLE_CENTER_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutA3', + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutB3', + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutC3', + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, + }, + { + cutoutId: 'cutoutD3', + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + }, +] + export const MoveLabwareOnDeck: Story = { render: args => ( ), args: { diff --git a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx index 55656526e1b..8f629cc5695 100644 --- a/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx +++ b/components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx @@ -7,40 +7,45 @@ import { LoadedModule, getDeckDefFromRobotType, getModuleDef2, + getPositionFromSlotId, LoadedLabware, } from '@opentrons/shared-data' import { COLORS } from '../../ui-style-constants' import { IDENTITY_AFFINE_TRANSFORM, multiplyMatrices } from '../utils' -import { StyledDeck } from './StyledDeck' +import { BaseDeck } from '../BaseDeck' import type { Coordinates, LabwareDefinition2, LabwareLocation, RobotType, - DeckSlot, DeckDefinition, + DeckConfiguration, } from '@opentrons/shared-data' import type { StyleProps } from '../../primitives' -import type { TrashLocation } from './FlexTrash' +import type { TrashCutoutId } from './FlexTrash' const getModulePosition = ( - orderedSlots: DeckSlot[], + deckDef: DeckDefinition, moduleId: string, - loadedModules: LoadedModule[], - deckId: DeckDefinition['otId'] + loadedModules: LoadedModule[] ): Coordinates | null => { const loadedModule = loadedModules.find(m => m.id === moduleId) if (loadedModule == null) return null - const modSlot = orderedSlots.find( + const modSlot = deckDef.locations.addressableAreas.find( s => s.id === loadedModule.location.slotName ) if (modSlot == null) return null - const [modX, modY] = modSlot.position + + const modPosition = getPositionFromSlotId(loadedModule.id, deckDef) + if (modPosition == null) return null + const [modX, modY] = modPosition + const deckSpecificAffineTransform = - getModuleDef2(loadedModule.model).slotTransforms?.[deckId]?.[modSlot.id] - ?.labwareOffset ?? IDENTITY_AFFINE_TRANSFORM + getModuleDef2(loadedModule.model).slotTransforms?.[deckDef.otId]?.[ + modSlot.id + ]?.labwareOffset ?? IDENTITY_AFFINE_TRANSFORM const [[labwareX], [labwareY], [labwareZ]] = multiplyMatrices( [[modX], [modY], [1], [1]], deckSpecificAffineTransform @@ -49,15 +54,13 @@ const getModulePosition = ( } function getLabwareCoordinates({ - orderedSlots, + deckDef, location, - deckId, loadedModules, loadedLabware, }: { - orderedSlots: DeckSlot[] + deckDef: DeckDefinition location: LabwareLocation - deckId: DeckDefinition['otId'] loadedModules: LoadedModule[] loadedLabware: LoadedLabware[] }): Coordinates | null { @@ -76,27 +79,43 @@ function getLabwareCoordinates({ // adapter on module if ('moduleId' in loadedAdapterLocation) { return getModulePosition( - orderedSlots, + deckDef, loadedAdapterLocation.moduleId, - loadedModules, - deckId + loadedModules ) } // adapter on deck - const loadedAdapterSlot = orderedSlots.find( - s => s.id === loadedAdapterLocation.slotName + const loadedAdapterSlotPosition = getPositionFromSlotId( + 'slotName' in loadedAdapterLocation + ? loadedAdapterLocation.slotName + : loadedAdapterLocation.addressableAreaName, + deckDef ) - return loadedAdapterSlot != null + return loadedAdapterSlotPosition != null ? { - x: loadedAdapterSlot.position[0], - y: loadedAdapterSlot.position[1], - z: loadedAdapterSlot.position[2], + x: loadedAdapterSlotPosition[0], + y: loadedAdapterSlotPosition[1], + z: loadedAdapterSlotPosition[2], + } + : null + } else if ('addressableAreaName' in location) { + const slotCoordinateTuple = getPositionFromSlotId( + location.addressableAreaName, + deckDef + ) + return slotCoordinateTuple != null + ? { + x: slotCoordinateTuple[0], + y: slotCoordinateTuple[1], + z: slotCoordinateTuple[2], } : null } else if ('slotName' in location) { - const slotCoordinateTuple = - orderedSlots.find(s => s.id === location.slotName)?.position ?? null + const slotCoordinateTuple = getPositionFromSlotId( + location.slotName, + deckDef + ) return slotCoordinateTuple != null ? { x: slotCoordinateTuple[0], @@ -105,12 +124,7 @@ function getLabwareCoordinates({ } : null } else { - return getModulePosition( - orderedSlots, - location.moduleId, - loadedModules, - deckId - ) + return getModulePosition(deckDef, location.moduleId, loadedModules) } } @@ -124,9 +138,10 @@ interface MoveLabwareOnDeckProps extends StyleProps { finalLabwareLocation: LabwareLocation loadedModules: LoadedModule[] loadedLabware: LoadedLabware[] + deckConfig: DeckConfiguration backgroundItems?: React.ReactNode deckFill?: string - trashLocation?: TrashLocation + trashCutoutId?: TrashCutoutId } export function MoveLabwareOnDeck( props: MoveLabwareOnDeckProps @@ -138,17 +153,28 @@ export function MoveLabwareOnDeck( initialLabwareLocation, finalLabwareLocation, loadedModules, + deckConfig, backgroundItems = null, - deckFill = '#e6e6e6', - trashLocation, ...styleProps } = props const deckDef = React.useMemo(() => getDeckDefFromRobotType(robotType), [ robotType, ]) + const initialSlotId = + initialLabwareLocation === 'offDeck' || + !('slotName' in initialLabwareLocation) + ? deckDef.locations.addressableAreas[1].id + : initialLabwareLocation.slotName + + const slotPosition = getPositionFromSlotId(initialSlotId, deckDef) ?? [ + 0, + 0, + 0, + ] + const offDeckPosition = { - x: deckDef.locations.orderedSlots[1].position[0], + x: slotPosition[0], y: deckDef.cornerOffsetFromOrigin[1] - movedLabwareDef.dimensions.xDimension - @@ -156,18 +182,16 @@ export function MoveLabwareOnDeck( } const initialPosition = getLabwareCoordinates({ - orderedSlots: deckDef.locations.orderedSlots, + deckDef, location: initialLabwareLocation, loadedModules, - deckId: deckDef.otId, loadedLabware, }) ?? offDeckPosition const finalPosition = getLabwareCoordinates({ - orderedSlots: deckDef.locations.orderedSlots, + deckDef, location: finalLabwareLocation, loadedModules, - deckId: deckDef.otId, loadedLabware, }) ?? offDeckPosition @@ -192,26 +216,16 @@ export function MoveLabwareOnDeck( if (deckDef == null) return null - const [viewBoxOriginX, viewBoxOriginY] = deckDef.cornerOffsetFromOrigin - const [deckXDimension, deckYDimension] = deckDef.dimensions - const wholeDeckViewBox = `${viewBoxOriginX} ${viewBoxOriginY} ${deckXDimension} ${deckYDimension}` - return ( - - {deckDef != null && ( - - )} {backgroundItems} - + ) } @@ -260,7 +274,6 @@ export function MoveLabwareOnDeck( * These animated components needs to be split out because react-spring and styled-components don't play nice * @see https://github.com/pmndrs/react-spring/issues/1515 */ const AnimatedG = styled(animated.g)`` -const AnimatedSvg = styled(animated.svg)`` interface WellProps { wellDef: LabwareWell diff --git a/components/src/hardware-sim/Deck/RobotWorkSpace.tsx b/components/src/hardware-sim/Deck/RobotWorkSpace.tsx index 5f36dababc5..3678d1acfea 100644 --- a/components/src/hardware-sim/Deck/RobotWorkSpace.tsx +++ b/components/src/hardware-sim/Deck/RobotWorkSpace.tsx @@ -1,9 +1,10 @@ import * as React from 'react' +import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { StyleProps, Svg } from '../../primitives' import { StyledDeck } from './StyledDeck' import type { DeckDefinition, DeckSlot } from '@opentrons/shared-data' -import type { TrashLocation } from './FlexTrash' +import type { TrashCutoutId } from './FlexTrash' export interface RobotWorkSpaceRenderProps { deckSlotsById: { [slotId: string]: DeckSlot } @@ -19,8 +20,10 @@ export interface RobotWorkSpaceProps extends StyleProps { children?: (props: RobotWorkSpaceRenderProps) => React.ReactNode deckFill?: string deckLayerBlocklist?: string[] + // optional boolean to show the OT-2 deck from deck defintion layers + showDeckLayers?: boolean // TODO(bh, 2023-10-09): remove - trashSlotName?: TrashLocation + trashCutoutId?: TrashCutoutId trashColor?: string id?: string } @@ -33,7 +36,8 @@ export function RobotWorkSpace(props: RobotWorkSpaceProps): JSX.Element | null { deckDef, deckFill = '#CCCCCC', deckLayerBlocklist = [], - trashSlotName, + showDeckLayers = false, + trashCutoutId, viewBox, trashColor, id, @@ -65,7 +69,7 @@ export function RobotWorkSpace(props: RobotWorkSpaceProps): JSX.Element | null { const [viewBoxOriginX, viewBoxOriginY] = deckDef.cornerOffsetFromOrigin const [deckXDimension, deckYDimension] = deckDef.dimensions - deckSlotsById = deckDef.locations.orderedSlots.reduce( + deckSlotsById = deckDef.locations.addressableAreas.reduce( (acc, deckSlot) => ({ ...acc, [deckSlot.id]: deckSlot }), {} ) @@ -80,15 +84,15 @@ export function RobotWorkSpace(props: RobotWorkSpaceProps): JSX.Element | null { transform="scale(1, -1)" {...styleProps} > - {deckDef != null && ( + {showDeckLayers ? ( - )} + ) : null} {children?.({ deckSlotsById, getRobotCoordsFromDOMCoords })} ) diff --git a/components/src/hardware-sim/Deck/SlotLabels.tsx b/components/src/hardware-sim/Deck/SlotLabels.tsx index 4bfe059ed3f..31648cda9c0 100644 --- a/components/src/hardware-sim/Deck/SlotLabels.tsx +++ b/components/src/hardware-sim/Deck/SlotLabels.tsx @@ -10,6 +10,7 @@ import type { RobotType } from '@opentrons/shared-data' interface SlotLabelsProps { robotType: RobotType color?: string + show4thColumn?: boolean } /** @@ -19,7 +20,11 @@ interface SlotLabelsProps { export const SlotLabels = ({ robotType, color, + show4thColumn = false, }: SlotLabelsProps): JSX.Element | null => { + const widthSmallRem = 10.5 + const widthLargeRem = 15.25 + return robotType === FLEX_ROBOT_TYPE ? ( <> + {show4thColumn ? ( + + + + ) : null} diff --git a/components/src/hardware-sim/Deck/StyledDeck.tsx b/components/src/hardware-sim/Deck/StyledDeck.tsx index 55e1cbfc3c9..7ac0130fbc0 100644 --- a/components/src/hardware-sim/Deck/StyledDeck.tsx +++ b/components/src/hardware-sim/Deck/StyledDeck.tsx @@ -1,57 +1,59 @@ import * as React from 'react' import styled from 'styled-components' -import { DeckFromData } from './DeckFromData' +import { DeckFromLayers } from './DeckFromLayers' import { FlexTrash } from './FlexTrash' -import type { DeckFromDataProps } from './DeckFromData' -import type { TrashLocation } from './FlexTrash' +import type { RobotType } from '@opentrons/shared-data' +import type { DeckFromLayersProps } from './DeckFromLayers' +import type { TrashCutoutId } from './FlexTrash' interface StyledDeckProps { deckFill: string + robotType: RobotType trashColor?: string - trashLocation?: TrashLocation + trashCutoutId?: TrashCutoutId } // apply fill to .SLOT_BASE class from ot3_standard deck definition -const StyledG = styled.g` +const StyledG = styled.g>` .SLOT_BASE { fill: ${props => props.deckFill}; } ` export function StyledDeck( - props: StyledDeckProps & DeckFromDataProps + props: StyledDeckProps & DeckFromLayersProps ): JSX.Element { const { deckFill, - trashLocation, + robotType, + trashCutoutId, trashColor = '#757070', - ...deckFromDataProps + ...DeckFromLayersProps } = props - - const robotType = deckFromDataProps.def.robot.model ?? 'OT-2 Standard' - const trashSlotClipId = - trashLocation != null ? `SLOT_CLIPS_${trashLocation}` : null + trashCutoutId != null ? `SLOT_CLIPS_${trashCutoutId}` : null const trashLayerBlocklist = trashSlotClipId != null - ? deckFromDataProps.layerBlocklist.concat(trashSlotClipId) - : deckFromDataProps.layerBlocklist + ? DeckFromLayersProps.layerBlocklist.concat(trashSlotClipId) + : DeckFromLayersProps.layerBlocklist return ( - - {trashLocation != null ? ( + {/* TODO(bh, 2023-11-06): remove trash and trashCutoutId prop when StyledDeck removed from MoveLabwareOnDeck */} + {trashCutoutId != null ? ( ) : null} diff --git a/components/src/hardware-sim/Deck/getDeckDefinitions.ts b/components/src/hardware-sim/Deck/getDeckDefinitions.ts index 0d440a4188e..b0be5266015 100644 --- a/components/src/hardware-sim/Deck/getDeckDefinitions.ts +++ b/components/src/hardware-sim/Deck/getDeckDefinitions.ts @@ -6,7 +6,7 @@ import type { DeckDefinition } from '@opentrons/shared-data' // and SD is not webpacked const deckDefinitionsContext = require.context( - '@opentrons/shared-data/deck/definitions/3', + '@opentrons/shared-data/deck/definitions/4', true, // traverse subdirectories /\.json$/, // import filter 'sync' // load every definition into one synchronous chunk diff --git a/components/src/hardware-sim/Deck/index.tsx b/components/src/hardware-sim/Deck/index.tsx index 1a5bfebefff..1d2b9b8fec7 100644 --- a/components/src/hardware-sim/Deck/index.tsx +++ b/components/src/hardware-sim/Deck/index.tsx @@ -1,4 +1,4 @@ -export * from './DeckFromData' +export * from './DeckFromLayers' export * from './FlexTrash' export * from './getDeckDefinitions' export * from './MoveLabwareOnDeck' diff --git a/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx b/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx index c28469d30b3..42b6d87b4e1 100644 --- a/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx +++ b/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx @@ -11,6 +11,7 @@ import { import { DeckConfigurator } from '.' import type { Story, Meta } from '@storybook/react' +import type { Fixture } from '@opentrons/shared-data' export default { title: 'Library/Molecules/Simulation/DeckConfigurator', @@ -19,44 +20,44 @@ export default { const Template: Story> = args => ( ) -const deckConfig = [ +const deckConfig: Fixture[] = [ { - fixtureLocation: 'A1', + fixtureLocation: 'cutoutA1', loadName: STANDARD_SLOT_LOAD_NAME, fixtureId: uuidv4(), }, { - fixtureLocation: 'B1', + fixtureLocation: 'cutoutB1', loadName: STANDARD_SLOT_LOAD_NAME, fixtureId: uuidv4(), }, { - fixtureLocation: 'C1', + fixtureLocation: 'cutoutC1', loadName: STANDARD_SLOT_LOAD_NAME, fixtureId: uuidv4(), }, { - fixtureLocation: 'D1', + fixtureLocation: 'cutoutD1', loadName: STANDARD_SLOT_LOAD_NAME, fixtureId: uuidv4(), }, { - fixtureLocation: 'A3', + fixtureLocation: 'cutoutA3', loadName: TRASH_BIN_LOAD_NAME, fixtureId: uuidv4(), }, { - fixtureLocation: 'B3', + fixtureLocation: 'cutoutB3', loadName: STANDARD_SLOT_LOAD_NAME, fixtureId: uuidv4(), }, { - fixtureLocation: 'C3', + fixtureLocation: 'cutoutC3', loadName: STAGING_AREA_LOAD_NAME, fixtureId: uuidv4(), }, { - fixtureLocation: 'D3', + fixtureLocation: 'cutoutD3', loadName: WASTE_CHUTE_LOAD_NAME, fixtureId: uuidv4(), }, diff --git a/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx index ef579ea5bda..c0294942b69 100644 --- a/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/EmptyConfigFixture.tsx @@ -1,37 +1,17 @@ import * as React from 'react' - -import { - getDeckDefFromRobotType, - FLEX_ROBOT_TYPE, -} from '@opentrons/shared-data' +import { css } from 'styled-components' import { Icon } from '../../icons' import { Btn, Flex } from '../../primitives' import { ALIGN_CENTER, DISPLAY_FLEX, JUSTIFY_CENTER } from '../../styles' import { BORDERS, COLORS } from '../../ui-style-constants' import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' +import { FIXTURE_HEIGHT, SINGLE_SLOT_FIXTURE_WIDTH } from './constants' -import type { Cutout } from '@opentrons/shared-data' - -// TODO: replace stubs with JSON definitions when available -const standardSlotDef = { - schemaVersion: 1, - version: 1, - namespace: 'opentrons', - metadata: { - displayName: 'standard slot', - }, - parameters: { - loadName: 'standard_slot', - }, - boundingBox: { - xDimension: 246.5, - yDimension: 106.0, - zDimension: 0, - }, -} +import type { Cutout, DeckDefinition } from '@opentrons/shared-data' interface EmptyConfigFixtureProps { + deckDefinition: DeckDefinition fixtureLocation: Cutout handleClickAdd: (fixtureLocation: Cutout) => void } @@ -39,11 +19,10 @@ interface EmptyConfigFixtureProps { export function EmptyConfigFixture( props: EmptyConfigFixtureProps ): JSX.Element { - const { handleClickAdd, fixtureLocation } = props - const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) + const { deckDefinition, handleClickAdd, fixtureLocation } = props // TODO: migrate to fixture location for v4 - const standardSlot = deckDef.locations.orderedSlots.find( + const standardSlot = deckDefinition.locations.cutouts.find( slot => slot.id === fixtureLocation ) const [xSlotPosition = 0, ySlotPosition = 0] = standardSlot?.position ?? [] @@ -51,34 +30,25 @@ export function EmptyConfigFixture( // TODO: remove adjustment when reading from fixture position // adjust x differently for right side/left side const isLeftSideofDeck = - fixtureLocation === 'A1' || - fixtureLocation === 'B1' || - fixtureLocation === 'C1' || - fixtureLocation === 'D1' + fixtureLocation === 'cutoutA1' || + fixtureLocation === 'cutoutB1' || + fixtureLocation === 'cutoutC1' || + fixtureLocation === 'cutoutD1' const xAdjustment = isLeftSideofDeck ? -101.5 : -17 const x = xSlotPosition + xAdjustment const yAdjustment = -10 const y = ySlotPosition + yAdjustment - const { xDimension, yDimension } = standardSlotDef.boundingBox - return ( - + ) } + +const EMPTY_CONFIG_STYLE = css` + align-items: ${ALIGN_CENTER}; + justify-content: ${JUSTIFY_CENTER}; + background-color: ${COLORS.mediumBlueEnabled}; + border: 3px dashed ${COLORS.blueEnabled}; + border-radius: ${BORDERS.radiusSoftCorners}; + width: 100%; + + &:active { + border: 3px solid ${COLORS.blueEnabled}; + background-color: ${COLORS.mediumBluePressed}; + } + + &:focus { + border: 3px solid ${COLORS.blueEnabled}; + background-color: ${COLORS.mediumBluePressed}; + } + + &:hover { + background-color: ${COLORS.mediumBluePressed}; + } + + &:focus-visible { + border: 3px solid ${COLORS.fundamentalsFocus}; + } +` diff --git a/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx index f8ba5b44e35..80cecfd71d5 100644 --- a/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/StagingAreaConfigFixture.tsx @@ -1,37 +1,21 @@ import * as React from 'react' - -import { - getDeckDefFromRobotType, - FLEX_ROBOT_TYPE, -} from '@opentrons/shared-data' +import { css } from 'styled-components' import { Icon } from '../../icons' import { Btn, Flex, Text } from '../../primitives' import { ALIGN_CENTER, DISPLAY_FLEX, JUSTIFY_CENTER } from '../../styles' import { BORDERS, COLORS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' +import { + FIXTURE_HEIGHT, + STAGING_AREA_DISPLAY_NAME, + STAGING_AREA_FIXTURE_WIDTH, +} from './constants' -import type { Cutout } from '@opentrons/shared-data' - -// TODO: replace stubs with JSON definitions when available -const stagingAreaDef = { - schemaVersion: 1, - version: 1, - namespace: 'opentrons', - metadata: { - displayName: 'Staging area', - }, - parameters: { - loadName: 'extension_slot', - }, - boundingBox: { - xDimension: 318.5, - yDimension: 106.0, - zDimension: 0, - }, -} +import type { Cutout, DeckDefinition } from '@opentrons/shared-data' interface StagingAreaConfigFixtureProps { + deckDefinition: DeckDefinition fixtureLocation: Cutout handleClickRemove?: (fixtureLocation: Cutout) => void } @@ -39,11 +23,9 @@ interface StagingAreaConfigFixtureProps { export function StagingAreaConfigFixture( props: StagingAreaConfigFixtureProps ): JSX.Element { - const { handleClickRemove, fixtureLocation } = props - const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) + const { deckDefinition, handleClickRemove, fixtureLocation } = props - // TODO: migrate to fixture location for v4 - const stagingAreaSlot = deckDef.locations.orderedSlots.find( + const stagingAreaSlot = deckDefinition.locations.cutouts.find( slot => slot.id === fixtureLocation ) const [xSlotPosition = 0, ySlotPosition = 0] = stagingAreaSlot?.position ?? [] @@ -53,28 +35,18 @@ export function StagingAreaConfigFixture( const yAdjustment = -10 const y = ySlotPosition + yAdjustment - const { xDimension, yDimension } = stagingAreaDef.boundingBox - return ( - - - {stagingAreaDef.metadata.displayName} + + + {STAGING_AREA_DISPLAY_NAME} {handleClickRemove != null ? ( ) } + +const STAGING_AREA_CONFIG_STYLE = css` + align-items: ${ALIGN_CENTER}; + background-color: ${COLORS.grey2}; + border-radius: ${BORDERS.borderRadiusSize1}; + color: ${COLORS.white}; + grid-gap: ${SPACING.spacing8}; + justify-content: ${JUSTIFY_CENTER}; + width: 100%; + + &:active { + background-color: ${COLORS.darkBlack90}; + } + + &:hover { + background-color: ${COLORS.grey1}; + } + + &:focus-visible { + border: 3px solid ${COLORS.fundamentalsFocus}; + } +` diff --git a/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx index 0d78b538fb7..c0c745e367e 100644 --- a/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/TrashBinConfigFixture.tsx @@ -1,37 +1,21 @@ import * as React from 'react' - -import { - getDeckDefFromRobotType, - FLEX_ROBOT_TYPE, -} from '@opentrons/shared-data' +import { css } from 'styled-components' import { Icon } from '../../icons' import { Btn, Flex, Text } from '../../primitives' import { ALIGN_CENTER, DISPLAY_FLEX, JUSTIFY_CENTER } from '../../styles' import { BORDERS, COLORS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' +import { + FIXTURE_HEIGHT, + SINGLE_SLOT_FIXTURE_WIDTH, + TRASH_BIN_DISPLAY_NAME, +} from './constants' -import type { Cutout } from '@opentrons/shared-data' - -// TODO: replace stubs with JSON definitions when available -const trashBinDef = { - schemaVersion: 1, - version: 1, - namespace: 'opentrons', - metadata: { - displayName: 'Trash bin', - }, - parameters: { - loadName: 'trash_bin', - }, - boundingBox: { - xDimension: 246.5, - yDimension: 106.0, - zDimension: 0, - }, -} +import type { Cutout, DeckDefinition } from '@opentrons/shared-data' interface TrashBinConfigFixtureProps { + deckDefinition: DeckDefinition fixtureLocation: Cutout handleClickRemove?: (fixtureLocation: Cutout) => void } @@ -39,48 +23,36 @@ interface TrashBinConfigFixtureProps { export function TrashBinConfigFixture( props: TrashBinConfigFixtureProps ): JSX.Element { - const { handleClickRemove, fixtureLocation } = props - const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) + const { deckDefinition, handleClickRemove, fixtureLocation } = props - // TODO: migrate to fixture location for v4 - const trashBinSlot = deckDef.locations.orderedSlots.find( + const trashBinSlot = deckDefinition.locations.cutouts.find( slot => slot.id === fixtureLocation ) const [xSlotPosition = 0, ySlotPosition = 0] = trashBinSlot?.position ?? [] // TODO: remove adjustment when reading from fixture position // adjust x differently for right side/left side const isLeftSideofDeck = - fixtureLocation === 'A1' || - fixtureLocation === 'B1' || - fixtureLocation === 'C1' || - fixtureLocation === 'D1' + fixtureLocation === 'cutoutA1' || + fixtureLocation === 'cutoutB1' || + fixtureLocation === 'cutoutC1' || + fixtureLocation === 'cutoutD1' const xAdjustment = isLeftSideofDeck ? -101.5 : -17 const x = xSlotPosition + xAdjustment const yAdjustment = -10 const y = ySlotPosition + yAdjustment - const { xDimension, yDimension } = trashBinDef.boundingBox - return ( - - - {trashBinDef.metadata.displayName} + + + {TRASH_BIN_DISPLAY_NAME} {handleClickRemove != null ? ( ) } + +const TRASH_BIN_CONFIG_STYLE = css` + align-items: ${ALIGN_CENTER}; + background-color: ${COLORS.grey2}; + border-radius: ${BORDERS.borderRadiusSize1}; + color: ${COLORS.white}; + justify-content: ${JUSTIFY_CENTER}; + grid-gap: ${SPACING.spacing8}; + width: 100%; + + &:active { + background-color: ${COLORS.darkBlack90}; + } + + &:hover { + background-color: ${COLORS.grey1}; + } + + &:focus-visible { + } +` diff --git a/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx b/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx index da0b9241df7..5e974d7eb8a 100644 --- a/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/WasteChuteConfigFixture.tsx @@ -1,49 +1,38 @@ import * as React from 'react' - -import { - getDeckDefFromRobotType, - FLEX_ROBOT_TYPE, -} from '@opentrons/shared-data' +import { css } from 'styled-components' import { Icon } from '../../icons' import { Btn, Flex, Text } from '../../primitives' import { ALIGN_CENTER, DISPLAY_FLEX, JUSTIFY_CENTER } from '../../styles' import { BORDERS, COLORS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject' +import { + WASTE_CHUTE_DISPLAY_NAME, + FIXTURE_HEIGHT, + STAGING_AREA_FIXTURE_WIDTH, + SINGLE_SLOT_FIXTURE_WIDTH, +} from './constants' -import type { Cutout } from '@opentrons/shared-data' - -// TODO: replace stubs with JSON definitions when available -const wasteChuteDef = { - schemaVersion: 1, - version: 1, - namespace: 'opentrons', - metadata: { - displayName: 'Waste chute', - }, - parameters: { - loadName: 'trash_chute', - }, - boundingBox: { - xDimension: 286.5, - yDimension: 106.0, - zDimension: 0, - }, -} +import type { Cutout, DeckDefinition } from '@opentrons/shared-data' interface WasteChuteConfigFixtureProps { + deckDefinition: DeckDefinition fixtureLocation: Cutout handleClickRemove?: (fixtureLocation: Cutout) => void + hasStagingAreas?: boolean } export function WasteChuteConfigFixture( props: WasteChuteConfigFixtureProps ): JSX.Element { - const { handleClickRemove, fixtureLocation } = props - const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) + const { + deckDefinition, + handleClickRemove, + fixtureLocation, + hasStagingAreas = false, + } = props - // TODO: migrate to fixture location for v4 - const wasteChuteSlot = deckDef.locations.orderedSlots.find( + const wasteChuteSlot = deckDefinition.locations.cutouts.find( slot => slot.id === fixtureLocation ) const [xSlotPosition = 0, ySlotPosition = 0] = wasteChuteSlot?.position ?? [] @@ -53,28 +42,20 @@ export function WasteChuteConfigFixture( const yAdjustment = -10 const y = ySlotPosition + yAdjustment - const { xDimension, yDimension } = wasteChuteDef.boundingBox - return ( - - - {wasteChuteDef.metadata.displayName} + + + {WASTE_CHUTE_DISPLAY_NAME} {handleClickRemove != null ? ( ) } + +const WASTE_CHUTE_CONFIG_STYLE = css` + align-items: ${ALIGN_CENTER}; + background-color: ${COLORS.grey2}; + border-radius: ${BORDERS.borderRadiusSize1}; + color: ${COLORS.white}; + justify-content: ${JUSTIFY_CENTER}; + grid-gap: ${SPACING.spacing8}; + width: 100%; + + &:active { + background-color: ${COLORS.darkBlack90}; + } + + &:hover { + background-color: ${COLORS.grey1}; + } + + &:focus-visible { + border: 3px solid ${COLORS.fundamentalsFocus}; + } +` diff --git a/components/src/hardware-sim/DeckConfigurator/constants.ts b/components/src/hardware-sim/DeckConfigurator/constants.ts new file mode 100644 index 00000000000..79f246274e3 --- /dev/null +++ b/components/src/hardware-sim/DeckConfigurator/constants.ts @@ -0,0 +1,6 @@ +export const FIXTURE_HEIGHT = 106.0 +export const SINGLE_SLOT_FIXTURE_WIDTH = 246.5 +export const STAGING_AREA_FIXTURE_WIDTH = 318.5 +export const STAGING_AREA_DISPLAY_NAME = 'Staging area' +export const TRASH_BIN_DISPLAY_NAME = 'Trash bin' +export const WASTE_CHUTE_DISPLAY_NAME = 'Waste chute' diff --git a/components/src/hardware-sim/DeckConfigurator/index.tsx b/components/src/hardware-sim/DeckConfigurator/index.tsx index 93d66920614..0e856e85d2a 100644 --- a/components/src/hardware-sim/DeckConfigurator/index.tsx +++ b/components/src/hardware-sim/DeckConfigurator/index.tsx @@ -3,10 +3,11 @@ import * as React from 'react' import { getDeckDefFromRobotType, FLEX_ROBOT_TYPE, - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + SINGLE_SLOT_FIXTURES, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_ONLY_FIXTURES, + WASTE_CHUTE_STAGING_AREA_FIXTURES, } from '@opentrons/shared-data' import { COLORS } from '../../ui-style-constants' @@ -18,12 +19,12 @@ import { StagingAreaConfigFixture } from './StagingAreaConfigFixture' import { TrashBinConfigFixture } from './TrashBinConfigFixture' import { WasteChuteConfigFixture } from './WasteChuteConfigFixture' -import type { Cutout, DeckConfiguration } from '@opentrons/shared-data' +import type { CutoutId, DeckConfiguration } from '@opentrons/shared-data' interface DeckConfiguratorProps { deckConfig: DeckConfiguration - handleClickAdd: (fixtureLocation: Cutout) => void - handleClickRemove: (fixtureLocation: Cutout) => void + handleClickAdd: (cutoutId: CutoutId) => void + handleClickRemove: (cutoutId: CutoutId) => void lightFill?: string darkFill?: string readOnly?: boolean @@ -45,33 +46,42 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) // restrict configuration to certain locations - const configurableFixtureLocations = [ - 'A1', - 'B1', - 'C1', - 'D1', - 'A3', - 'B3', - 'C3', - 'D3', + const configurableFixtureLocations: CutoutId[] = [ + 'cutoutA1', + 'cutoutB1', + 'cutoutC1', + 'cutoutD1', + 'cutoutA3', + 'cutoutB3', + 'cutoutC3', + 'cutoutD3', ] - const configurableDeckConfig = deckConfig.filter(fixture => - configurableFixtureLocations.includes(fixture.fixtureLocation) + const configurableDeckConfig = deckConfig.filter(({ cutoutId }) => + configurableFixtureLocations.includes(cutoutId) ) const stagingAreaFixtures = configurableDeckConfig.filter( - fixture => fixture.loadName === STAGING_AREA_LOAD_NAME + ({ cutoutFixtureId }) => cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE ) const wasteChuteFixtures = configurableDeckConfig.filter( - fixture => fixture.loadName === WASTE_CHUTE_LOAD_NAME + ({ cutoutFixtureId }) => + cutoutFixtureId != null && + WASTE_CHUTE_ONLY_FIXTURES.includes(cutoutFixtureId) + ) + const wasteChuteStagingAreaFixtures = configurableDeckConfig.filter( + ({ cutoutFixtureId }) => + cutoutFixtureId != null && + WASTE_CHUTE_STAGING_AREA_FIXTURES.includes(cutoutFixtureId) ) const emptyFixtures = readOnly ? [] : configurableDeckConfig.filter( - fixture => fixture.loadName === STANDARD_SLOT_LOAD_NAME + ({ cutoutFixtureId }) => + cutoutFixtureId != null && + SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) ) const trashBinFixtures = configurableDeckConfig.filter( - fixture => fixture.loadName === TRASH_BIN_LOAD_NAME + ({ cutoutFixtureId }) => cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE ) return ( @@ -80,46 +90,62 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions viewBox={`${deckDef.cornerOffsetFromOrigin[0]} ${deckDef.cornerOffsetFromOrigin[1]} ${deckDef.dimensions[0]} ${deckDef.dimensions[1]}`} > - {/* TODO(bh, 2023-10-18): migrate to v4 deck def cutouts */} - {deckDef.locations.orderedSlots.map(slotDef => ( + {deckDef.locations.cutouts.map(cutout => ( ))} - {stagingAreaFixtures.map(fixture => ( + {stagingAreaFixtures.map(({ cutoutId }) => ( ))} - {emptyFixtures.map(fixture => ( + {emptyFixtures.map(({ cutoutId }) => ( + ))} + {wasteChuteFixtures.map(({ cutoutId }) => ( + ))} - {wasteChuteFixtures.map(fixture => ( + {wasteChuteStagingAreaFixtures.map(({ cutoutId }) => ( ))} - {trashBinFixtures.map(fixture => ( + {trashBinFixtures.map(({ cutoutId }) => ( ))} - + 0} + /> {children} ) diff --git a/components/src/hardware-sim/DeckSlotLocation/index.tsx b/components/src/hardware-sim/DeckSlotLocation/index.tsx index 162a92df3e0..6f24bebf90f 100644 --- a/components/src/hardware-sim/DeckSlotLocation/index.tsx +++ b/components/src/hardware-sim/DeckSlotLocation/index.tsx @@ -25,7 +25,7 @@ export function DeckSlotLocation( ...restProps } = props - const slotDef = deckDefinition?.locations.orderedSlots.find( + const slotDef = deckDefinition?.locations.addressableAreas.find( s => s.id === slotName ) if (slotDef == null) { diff --git a/components/src/hardware-sim/Labware/LabwareRender.tsx b/components/src/hardware-sim/Labware/LabwareRender.tsx index ba515c73b94..798154da768 100644 --- a/components/src/hardware-sim/Labware/LabwareRender.tsx +++ b/components/src/hardware-sim/Labware/LabwareRender.tsx @@ -46,6 +46,8 @@ export interface LabwareRenderProps { wellStroke?: WellStroke /** CSS color to stroke the labware outline */ labwareStroke?: CSSProperties['stroke'] + /** adds thicker blue border with blur to labware */ + highlight?: boolean /** Optional callback, called with WellMouseEvent args onMouseEnter */ onMouseEnterWell?: (e: WellMouseEvent) => unknown /** Optional callback, called with WellMouseEvent args onMouseLeave */ @@ -54,10 +56,7 @@ export interface LabwareRenderProps { allows drag-to-select behavior */ selectableWellClass?: string gRef?: React.RefObject - // adds blue border with drop shadow to labware - hover?: boolean onLabwareClick?: () => void - highlightLabware?: boolean } export const LabwareRender = (props: LabwareRenderProps): JSX.Element => { @@ -74,22 +73,21 @@ export const LabwareRender = (props: LabwareRenderProps): JSX.Element => { onMouseEnterWell={props.onMouseEnterWell} onMouseLeaveWell={props.onMouseLeaveWell} selectableWellClass={props.selectableWellClass} - hover={props.hover} onLabwareClick={props.onLabwareClick} - highlightLabware={props.highlightLabware} + highlight={props.highlight} /> - {props.wellStroke && ( + {props.wellStroke != null ? ( - )} - {props.wellFill && ( + ) : null} + {props.wellFill != null ? ( - )} + ) : null} {props.disabledWells != null ? props.disabledWells.map((well, index) => ( { /> )) : null} - {props.highlightedWells && ( + {props.highlightedWells != null ? ( - )} - {props.selectedWells && ( + ) : null} + {props.selectedWells != null ? ( - )} - {props.missingTips && ( + ) : null} + {props.missingTips != null ? ( - )} - {props.wellLabelOption && - props.definition.metadata.displayCategory !== 'adapter' && ( - - )} + ) : null} + {props.wellLabelOption != null && + props.definition.metadata.displayCategory !== 'adapter' ? ( + + ) : null} ) } diff --git a/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.css b/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.css index b1170cbe7d8..e5edc1ef332 100644 --- a/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.css +++ b/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.css @@ -17,5 +17,5 @@ } .hover_outline { - stroke: #006cfa; + stroke: var(--c-blue-enabled); } diff --git a/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.tsx b/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.tsx index 307de80c595..b0b95390d95 100644 --- a/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.tsx +++ b/components/src/hardware-sim/Labware/labwareInternals/LabwareOutline.tsx @@ -1,19 +1,23 @@ import * as React from 'react' -import cx from 'classnames' import { SLOT_RENDER_WIDTH, SLOT_RENDER_HEIGHT } from '@opentrons/shared-data' -import styles from './LabwareOutline.css' +import { COLORS } from '../../../ui-style-constants' import type { CSSProperties } from 'styled-components' import type { LabwareDefinition2 } from '@opentrons/shared-data' export interface LabwareOutlineProps { + /** Labware definition to outline */ definition?: LabwareDefinition2 + /** x dimension in mm of this labware, used if definition doesn't supply dimensions, defaults to 127.76 */ width?: number + /** y dimension in mm of this labware, used if definition doesn't supply dimensions, defaults to 85.48 */ height?: number + /** if this labware is a tip rack, darken background and lighten borderx dimension in mm of this labware, used if definition doesn't supply dimensions, defaults to false */ isTiprack?: boolean - hover?: boolean - stroke?: CSSProperties['stroke'] + /** adds thicker blue border with blur to labware, defaults to false */ highlight?: boolean + /** [legacy] override the border color */ + stroke?: CSSProperties['stroke'] } const OUTLINE_THICKNESS_MM = 1 @@ -23,32 +27,73 @@ export function LabwareOutline(props: LabwareOutlineProps): JSX.Element { definition, width = SLOT_RENDER_WIDTH, height = SLOT_RENDER_HEIGHT, - isTiprack, + isTiprack = false, + highlight = false, stroke, - hover, - highlight, } = props const { parameters = { isTiprack }, dimensions = { xDimension: width, yDimension: height }, - } = definition || {} + } = definition ?? {} + const backgroundFill = parameters.isTiprack ? '#CCCCCC' : COLORS.white return ( <> - + {highlight ? ( + <> + + + + + + + + + ) : ( + + )} ) } + +interface LabwareBorderProps extends React.SVGProps { + borderThickness: number + xDimension: number + yDimension: number +} +function LabwareBorder(props: LabwareBorderProps): JSX.Element { + const { borderThickness, xDimension, yDimension, ...svgProps } = props + return ( + + ) +} diff --git a/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx b/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx index 0e7994969a6..0da23c1cc35 100644 --- a/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx +++ b/components/src/hardware-sim/Labware/labwareInternals/StaticLabware.tsx @@ -11,22 +11,25 @@ import type { LabwareDefinition2, LabwareWell } from '@opentrons/shared-data' import type { WellMouseEvent } from './types' export interface StaticLabwareProps { + /** Labware definition to render */ definition: LabwareDefinition2 - selectableWellClass?: string + /** Add thicker blurred blue border to labware, defaults to false */ + highlight?: boolean + /** Optional callback to be executed when entire labware element is clicked */ + onLabwareClick?: () => void + /** Optional callback to be executed when mouse enters a well element */ onMouseEnterWell?: (e: WellMouseEvent) => unknown + /** Optional callback to be executed when mouse leaves a well element */ onMouseLeaveWell?: (e: WellMouseEvent) => unknown - hover?: boolean - onLabwareClick?: () => void - highlightLabware?: boolean + /** [legacy] css class to be added to well component if it is selectable */ + selectableWellClass?: string } const TipDecoration = React.memo(function TipDecoration(props: { well: LabwareWell }) { const { well } = props - // @ts-expect-error(mc, 2021-04-27): refine well type before accessing `diameter` - if (well.diameter) { - // @ts-expect-error(mc, 2021-04-27): refine well type before accessing `diameter` + if ('diameter' in well && well.diameter != null) { const radius = well.diameter / 2 return ( @@ -38,14 +41,12 @@ const TipDecoration = React.memo(function TipDecoration(props: { export function StaticLabwareComponent(props: StaticLabwareProps): JSX.Element { const { isTiprack } = props.definition.parameters - return ( diff --git a/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpace.tsx b/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpace.tsx index 4ac35b2aed5..6a145c8bd67 100644 --- a/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpace.tsx +++ b/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpace.tsx @@ -1,13 +1,27 @@ +import styled from 'styled-components' +import { animated } from '@react-spring/web' import * as React from 'react' import { Svg } from '../../primitives' +interface RobotCoordinateSpaceProps extends React.ComponentProps { + animated?: boolean +} export function RobotCoordinateSpace( - props: React.ComponentProps + props: RobotCoordinateSpaceProps ): JSX.Element { - const { children, ...restProps } = props - return ( - - {children} - + const { animated = false, children, ...restProps } = props + const allPassThroughProps = { + transform: 'scale(1, -1)', + ...restProps, + } + return animated ? ( + {children} + ) : ( + {children} ) } + +/** + * These animated components needs to be split out because react-spring and styled-components don't play nice + * @see https://github.com/pmndrs/react-spring/issues/1515 */ +const AnimatedSvg = styled(animated.svg)`` diff --git a/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithDOMCoords.tsx b/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithDOMCoords.tsx index cd73a30b371..5ca8396c5be 100644 --- a/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithDOMCoords.tsx +++ b/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithDOMCoords.tsx @@ -46,7 +46,7 @@ export function RobotCoordinateSpaceWithDOMCoords( const [viewBoxOriginX, viewBoxOriginY] = deckDef.cornerOffsetFromOrigin const [deckXDimension, deckYDimension] = deckDef.dimensions - deckSlotsById = deckDef.locations.orderedSlots.reduce( + deckSlotsById = deckDef.locations.addressableAreas.reduce( (acc, deckSlot) => ({ ...acc, [deckSlot.id]: deckSlot }), {} ) diff --git a/components/src/hardware-sim/index.ts b/components/src/hardware-sim/index.ts index bf31daf32c7..309cad747d5 100644 --- a/components/src/hardware-sim/index.ts +++ b/components/src/hardware-sim/index.ts @@ -2,6 +2,7 @@ export * from './BaseDeck' export * from './BaseDeck/__fixtures__' export * from './Deck' export * from './DeckConfigurator' +export * from './DeckSlotLocation' export * from './Labware' export * from './Module' export * from './Pipette' diff --git a/components/src/hooks/useSelectDeckLocation/index.tsx b/components/src/hooks/useSelectDeckLocation/index.tsx index f571a5c9603..6a801765a33 100644 --- a/components/src/hooks/useSelectDeckLocation/index.tsx +++ b/components/src/hooks/useSelectDeckLocation/index.tsx @@ -1,15 +1,29 @@ import * as React from 'react' import isEqual from 'lodash/isEqual' -import { DeckDefinition, getDeckDefFromRobotType } from '@opentrons/shared-data' -import { RobotCoordinateSpace } from '../../hardware-sim/RobotCoordinateSpace' -import type { ModuleLocation, RobotType } from '@opentrons/shared-data' -import { COLORS, SPACING } from '../../ui-style-constants' -import { RobotCoordsForeignDiv, SlotLabels } from '../../hardware-sim' +import { + FLEX_CUTOUT_BY_SLOT_ID, + getDeckDefFromRobotType, + getPositionFromSlotId, + isAddressableAreaStandardSlot, +} from '@opentrons/shared-data' + +import { + RobotCoordinateSpace, + RobotCoordsForeignDiv, + SingleSlotFixture, + SlotLabels, +} from '../../hardware-sim' import { Icon } from '../../icons' import { Text } from '../../primitives' import { ALIGN_CENTER, JUSTIFY_CENTER } from '../../styles' -import { DeckSlotLocation } from '../../hardware-sim/DeckSlotLocation' +import { COLORS, SPACING } from '../../ui-style-constants' + +import type { + DeckDefinition, + ModuleLocation, + RobotType, +} from '@opentrons/shared-data' export type DeckLocationSelectThemes = 'default' | 'grey' @@ -26,7 +40,7 @@ export function useDeckLocationSelect( selectedLocation, setSelectedLocation, ] = React.useState({ - slotName: deckDef.locations.orderedSlots[0].id, + slotName: deckDef.locations.addressableAreas[0].id, }) return { DeckLocationSelect: ( @@ -60,82 +74,99 @@ export function DeckLocationSelect({ deckDef.cornerOffsetFromOrigin[1] } ${deckDef.dimensions[0] - X_CROP_MM * 2} ${deckDef.dimensions[1]}`} > - {deckDef.locations.orderedSlots.map(slot => { - const slotLocation = { slotName: slot.id } - const isDisabled = disabledLocations.some( - l => - typeof l === 'object' && 'slotName' in l && l.slotName === slot.id + {deckDef.locations.addressableAreas + // only render standard slot fixture components + .filter(addressableArea => + isAddressableAreaStandardSlot(addressableArea.id, deckDef) ) - const isSelected = isEqual(selectedLocation, slotLocation) - let fill = - theme === 'default' - ? COLORS.highlightPurple2 - : COLORS.lightGreyPressed - if (isSelected) - fill = + .map(slot => { + const slotLocation = { slotName: slot.id } + const isDisabled = disabledLocations.some( + l => + typeof l === 'object' && 'slotName' in l && l.slotName === slot.id + ) + const isSelected = isEqual(selectedLocation, slotLocation) + let fill = theme === 'default' - ? COLORS.highlightPurple1 - : COLORS.darkGreyEnabled - if (isDisabled) fill = COLORS.darkGreyDisabled - if (isSelected && slot.id === 'B1' && isThermocycler) { + ? COLORS.highlightPurple2 + : COLORS.lightGreyPressed + if (isSelected) + fill = + theme === 'default' + ? COLORS.highlightPurple1 + : COLORS.darkGreyEnabled + if (isDisabled) fill = COLORS.darkGreyDisabled + if (isSelected && slot.id === 'B1' && isThermocycler) { + return ( + + + + + + Selected + + + + ) + } else if (slot.id === 'A1' && isThermocycler) { + return null + } + + const slotPosition = getPositionFromSlotId(slot.id, deckDef) + const cutoutId = FLEX_CUTOUT_BY_SLOT_ID[slot.id] + return ( - - + + !isDisabled && + setSelectedLocation != null && + setSelectedLocation(slotLocation) + } + cursor={ + setSelectedLocation == null || isDisabled || isSelected + ? 'default' + : 'pointer' + } + deckDefinition={deckDef} /> - - - - Selected - - - + {isSelected && slotPosition != null ? ( + + + + Selected + + + ) : null} + ) - } else if (slot.id === 'A1' && isThermocycler) { - return null - } - return ( - - - !isDisabled && - setSelectedLocation != null && - setSelectedLocation(slotLocation) - } - cursor={ - setSelectedLocation == null || isDisabled || isSelected - ? 'default' - : 'pointer' - } - deckDefinition={deckDef} - /> - {isSelected ? ( - - - - Selected - - - ) : null} - - ) - })} + })} str: + """Convert list of something into CSV line.""" + return ",".join(str(elements)) diff --git a/hardware-testing/hardware_testing/drivers/asair_sensor.py b/hardware-testing/hardware_testing/drivers/asair_sensor.py index eb9a360eace..4e30c743045 100644 --- a/hardware-testing/hardware_testing/drivers/asair_sensor.py +++ b/hardware-testing/hardware_testing/drivers/asair_sensor.py @@ -16,6 +16,8 @@ from serial.serialutil import SerialException # type: ignore[import] from hardware_testing.data import ui +from serial.tools.list_ports import comports # type: ignore[import] + log = logging.getLogger(__name__) USB_VID = 0x0403 @@ -72,17 +74,32 @@ def get_serial(self) -> str: ... -def BuildAsairSensor(simulate: bool) -> AsairSensorBase: +def BuildAsairSensor(simulate: bool, autosearch: bool = True) -> AsairSensorBase: """Try to find and return an Asair sensor, if not found return a simulator.""" ui.print_title("Connecting to Environmental sensor") if not simulate: - port = list_ports_and_select(device_name="Asair environmental sensor") - try: + if not autosearch: + port = list_ports_and_select(device_name="Asair environmental sensor") sensor = AsairSensor.connect(port) ui.print_info(f"Found sensor on port {port}") return sensor - except SerialException: - pass + else: + ports = comports() + assert ports + for _port in ports: + port = _port.device # type: ignore[attr-defined] + try: + ui.print_info(f"Trying to connect to env sensor on port {port}") + sensor = AsairSensor.connect(port) + ser_id = sensor.get_serial() + ui.print_info(f"Found env sensor {ser_id} on port {port}") + return sensor + except: # noqa: E722 + pass + use_sim = ui.get_user_answer("No env sensor found, use simulator?") + if not use_sim: + raise SerialException("No sensor found") + ui.print_info("no sensor found returning simulator") return SimAsairSensor() diff --git a/hardware-testing/hardware_testing/drivers/pressure_fixture.py b/hardware-testing/hardware_testing/drivers/pressure_fixture.py index 82b5ccbb2d3..7743a433534 100644 --- a/hardware-testing/hardware_testing/drivers/pressure_fixture.py +++ b/hardware-testing/hardware_testing/drivers/pressure_fixture.py @@ -6,8 +6,13 @@ from typing import List, Tuple from typing_extensions import Final, Literal +from hardware_testing.data import ui from opentrons.types import Point +from serial.tools.list_ports import comports # type: ignore[import] +from serial import SerialException +from hardware_testing.drivers import list_ports_and_select + FIXTURE_REBOOT_TIME = 2 FIXTURE_NUM_CHANNELS: Final[int] = 8 FIXTURE_BAUD_RATE: Final[int] = 115200 @@ -98,6 +103,40 @@ def read_all_pressure_channel(self) -> List[float]: return pressure +def connect_to_fixture( + simulate: bool, side: str = "left", autosearch: bool = True +) -> PressureFixtureBase: + """Try to find and return an presure fixture, if not found return a simulator.""" + ui.print_title("Connecting to presure fixture") + if not simulate: + if not autosearch: + port = list_ports_and_select(device_name="Pressure fixture") + fixture = PressureFixture.create(port=port, slot_side=side) + fixture.connect() + ui.print_info(f"Found fixture on port {port}") + return fixture + else: + ports = comports() + assert ports + for _port in ports: + port = _port.device # type: ignore[attr-defined] + try: + ui.print_info( + f"Trying to connect to Pressure fixture on port {port}" + ) + fixture = PressureFixture.create(port=port, slot_side=side) + fixture.connect() + ui.print_info(f"Found fixture on port {port}") + return fixture + except: # noqa: E722 + pass + use_sim = ui.get_user_answer("No pressure sensor found, use simulator?") + if not use_sim: + raise SerialException("No sensor found") + ui.print_info("no fixture found returning simulator") + return SimPressureFixture() + + class PressureFixture(PressureFixtureBase): """OT3 Pressure Fixture Driver.""" diff --git a/hardware-testing/hardware_testing/gravimetric/__main__.py b/hardware-testing/hardware_testing/gravimetric/__main__.py index 2d790535445..8e9a7b1ff45 100644 --- a/hardware-testing/hardware_testing/gravimetric/__main__.py +++ b/hardware-testing/hardware_testing/gravimetric/__main__.py @@ -2,8 +2,10 @@ from json import load as json_load from pathlib import Path import argparse +from time import time from typing import List, Union, Dict, Optional, Any, Tuple from dataclasses import dataclass +from opentrons.hardware_control.types import OT3Mount from opentrons.protocol_api import ProtocolContext from . import report import subprocess @@ -40,6 +42,7 @@ from .measurement.record import GravimetricRecorder from .measurement import DELAY_FOR_MEASUREMENT from .measurement.scale import Scale +from .measurement.environment import read_environment_data from .trial import TestResources, _change_pipettes from .tips import get_tips from hardware_testing.drivers import asair_sensor @@ -192,6 +195,7 @@ def build_run_args(cls, args: argparse.Namespace) -> "RunArgs": args.pipette, "left", args.increment, + args.photometric, args.gantry_speed if not args.photometric else None, ) pipette_tag = helpers._get_tag_from_pipette( @@ -570,6 +574,7 @@ def _main( parser.add_argument( "--mode", type=str, choices=["", "default", "lowVolumeDefault"], default="" ) + parser.add_argument("--pre-heat", action="store_true") args = parser.parse_args() run_args = RunArgs.build_run_args(args) if not run_args.ctx.is_simulating(): @@ -580,14 +585,50 @@ def _main( shell=True, ) sleep(1) + hw = run_args.ctx._core.get_hardware() try: if not run_args.ctx.is_simulating() and not args.photometric: ui.get_user_ready("CLOSE the door, and MOVE AWAY from machine") ui.print_info("homing...") run_args.ctx.home() + + if args.pre_heat: + ui.print_header("PRE-HEAT") + mnt = OT3Mount.LEFT + hw.add_tip(mnt, 1) + hw.prepare_for_aspirate(mnt) + env_data = read_environment_data( + mnt.name.lower(), hw.is_simulator, run_args.environment_sensor + ) + start_temp = env_data.celsius_pipette + temp_limit = min(start_temp + 3.0, 28.0) + max_pre_heat_seconds = 60 * 10 + now = time() + start_time = now + while ( + now - start_time < max_pre_heat_seconds + and env_data.celsius_pipette < temp_limit + ): + ui.print_info( + f"pre-heat {int(now - start_time)} seconds " + f"({max_pre_heat_seconds} limit): " + f"{round(env_data.celsius_pipette, 2)} C " + f"({round(temp_limit, 2)} C limit)" + ) + # NOTE: moving slowly helps make sure full current is sent to coils + hw.aspirate(mnt, rate=0.1) + hw.dispense(mnt, rate=0.1, push_out=0) + env_data = read_environment_data( + mnt.name.lower(), hw.is_simulator, run_args.environment_sensor + ) + if run_args.ctx.is_simulating(): + now += 1 + else: + now = time() + hw.remove_tip(mnt) + for tip, volumes in run_args.volumes: if args.channels == 96 and not run_args.ctx.is_simulating(): - hw = run_args.ctx._core.get_hardware() ui.alert_user_ready(f"prepare the {tip}ul tipracks", hw) _main(args, run_args, tip, volumes) finally: @@ -598,5 +639,5 @@ def _main( _change_pipettes(run_args.ctx, run_args.pipette) if not run_args.ctx.is_simulating(): serial_logger.terminate() - del run_args.ctx._core.get_hardware()._backend.eeprom_driver._gpio + del hw._backend.eeprom_driver._gpio print("done\n\n") diff --git a/hardware-testing/hardware_testing/gravimetric/config.py b/hardware-testing/hardware_testing/gravimetric/config.py index a1febb6ddc7..e64259df908 100644 --- a/hardware-testing/hardware_testing/gravimetric/config.py +++ b/hardware-testing/hardware_testing/gravimetric/config.py @@ -301,6 +301,7 @@ def _get_liquid_probe_settings( }, ConfigType.photometric: { 1: 8, + 8: 12, 96: 5, }, } diff --git a/hardware-testing/hardware_testing/gravimetric/daily_setup.py b/hardware-testing/hardware_testing/gravimetric/daily_setup.py index 4e3eb26ba0d..bc13dc9d0bf 100644 --- a/hardware-testing/hardware_testing/gravimetric/daily_setup.py +++ b/hardware-testing/hardware_testing/gravimetric/daily_setup.py @@ -102,7 +102,6 @@ def _check_unstable_count(tag: str) -> None: OT3Mount.LEFT, Point(x=MOVE_X_MM, y=MOVE_Y_MM), speed=GANTRY_MAX_SPEED ) _check_unstable_count(tag) - hw.set_status_bar_state(COLOR_STATES["idle"]) # WALKING @@ -114,13 +113,12 @@ def _check_unstable_count(tag: str) -> None: if not hw.is_simulator: ui.get_user_ready("prepare to WALK") tag = "WALKING" + hw.set_status_bar_state(COLOR_STATES["walking"]) with recorder.samples_of_tag(tag): - num_disco_cycles = int(WALKING_SECONDS / 5) - for _ in range(num_disco_cycles): - hw.set_status_bar_state(COLOR_STATES["walking"]) - if not hw.is_simulator: - sleep(5) + if not hw.is_simulator: + sleep(WALKING_SECONDS) _check_unstable_count(tag) + hw.set_status_bar_state(COLOR_STATES["idle"]) def _wait_for_stability( @@ -155,7 +153,10 @@ def _wait_for_stability( def _run( - hw_api: SyncHardwareAPI, recorder: GravimetricRecorder, skip_stability: bool + hw_api: SyncHardwareAPI, + recorder: GravimetricRecorder, + skip_stability: bool, + calibrate: bool = False, ) -> None: ui.print_title("GRAVIMETRIC DAILY SETUP") ui.print_info(f"Scale: {recorder.max_capacity}g (SN:{recorder.serial_number})") @@ -185,13 +186,15 @@ def _calibrate() -> None: _record() hw_api.set_status_bar_state(COLOR_STATES["interact"]) if not hw_api.is_simulator: - ui.get_user_ready("INSTALL Radwag's default weighing pan") + if calibrate: + ui.get_user_ready("INSTALL Radwag's default weighing pan") ui.get_user_ready("REMOVE all weights, vials, and labware from scale") ui.get_user_ready("CLOSE door and step away from fixture") hw_api.set_status_bar_state(COLOR_STATES["idle"]) ui.print_header("TEST STABILITY") if not skip_stability: + print("homing...") hw_api.home() _wait_for_stability(recorder, hw_api, tag="stability") _test_stability(recorder, hw_api) @@ -204,12 +207,13 @@ def _calibrate() -> None: _wait_for_stability(recorder, hw_api, tag="zero") _zero() - ui.print_header("CALIBRATE SCALE") - if hw_api.is_simulator or ui.get_user_answer("calibrate (ADJUST) this scale"): - if not hw_api.is_simulator: - ui.get_user_ready("about to CALIBRATE the scale:") - _wait_for_stability(recorder, hw_api, tag="calibrate") - _calibrate() + if calibrate: + ui.print_header("CALIBRATE SCALE") + if hw_api.is_simulator or ui.get_user_answer("calibrate (ADJUST) this scale"): + if not hw_api.is_simulator: + ui.get_user_ready("about to CALIBRATE the scale:") + _wait_for_stability(recorder, hw_api, tag="calibrate") + _calibrate() ui.print_header("TEST ACCURACY") if hw_api.is_simulator: @@ -242,6 +246,7 @@ def _calibrate() -> None: if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--simulate", action="store_true") + parser.add_argument("--calibrate", action="store_true") parser.add_argument("--skip-stability", action="store_true") args = parser.parse_args() _ctx = helpers.get_api_context( @@ -263,15 +268,19 @@ def _calibrate() -> None: simulate=_hw.is_simulator, ) try: - _run(_hw, _rec, args.skip_stability) + _run(_hw, _rec, args.skip_stability, args.calibrate) _hw.set_status_bar_state(COLOR_STATES["pass"]) ui.print_header("Result: PASS") + if not args.simulate: + ui.get_user_ready("test done") + except KeyboardInterrupt: + ui.print_header("Result: EXITED EARLY") except Exception as e: _hw.set_status_bar_state(COLOR_STATES["fail"]) ui.print_header("Result: FAIL") - raise e - finally: if not args.simulate: ui.get_user_ready("test done") + raise e + finally: _rec.stop() _hw.set_status_bar_state(COLOR_STATES["idle"]) diff --git a/hardware-testing/hardware_testing/gravimetric/execute.py b/hardware-testing/hardware_testing/gravimetric/execute.py index d1a72f4e4c9..cf2b8fb1ecc 100644 --- a/hardware-testing/hardware_testing/gravimetric/execute.py +++ b/hardware-testing/hardware_testing/gravimetric/execute.py @@ -52,6 +52,8 @@ from .tips import MULTI_CHANNEL_TEST_ORDER import glob +from opentrons.hardware_control.types import StatusBarState + _MEASUREMENTS: List[Tuple[str, MeasurementData]] = list() _PREV_TRIAL_GRAMS: Optional[MeasurementData] = None @@ -593,7 +595,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq recorder._recording = GravimetricRecording() report.store_config_gm(resources.test_report, cfg) calibration_tip_in_use = True - + hw_api = resources.ctx._core.get_hardware() if resources.ctx.is_simulating(): _PREV_TRIAL_GRAMS = None _MEASUREMENTS = list() @@ -621,6 +623,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq average_aspirate_evaporation_ul = 0.0 average_dispense_evaporation_ul = 0.0 else: + hw_api.set_status_bar_state(StatusBarState.SOFTWARE_ERROR) ( average_aspirate_evaporation_ul, average_dispense_evaporation_ul, @@ -632,7 +635,7 @@ def run(cfg: config.GravimetricConfig, resources: TestResources) -> None: # noq resources.test_report, labware_on_scale, ) - + hw_api.set_status_bar_state(StatusBarState.IDLE) ui.print_info("dropping tip") if not cfg.same_tip: _drop_tip( diff --git a/hardware-testing/hardware_testing/gravimetric/execute_photometric.py b/hardware-testing/hardware_testing/gravimetric/execute_photometric.py index c2c5f314428..5b36acc46f3 100644 --- a/hardware-testing/hardware_testing/gravimetric/execute_photometric.py +++ b/hardware-testing/hardware_testing/gravimetric/execute_photometric.py @@ -319,7 +319,7 @@ def execute_trials( trial_count = 0 for volume in trials.keys(): ui.print_title(f"{volume} uL") - if cfg.pipette_channels == 1 and not resources.ctx.is_simulating(): + if cfg.pipette_channels != 96 and not resources.ctx.is_simulating(): ui.get_user_ready( f"put PLATE with prepped column {cfg.photoplate_column_offset} and remove SEAL" ) @@ -336,7 +336,7 @@ def execute_trials( resources.ctx, resources.pipette, cfg, location=next_tip_location ) _run_trial(trial) - if not trial.ctx.is_simulating() and trial.channel_count == 1: + if not trial.ctx.is_simulating() and trial.channel_count != 96: ui.get_user_ready("add SEAL to plate and remove from DECK") diff --git a/hardware-testing/hardware_testing/gravimetric/helpers.py b/hardware-testing/hardware_testing/gravimetric/helpers.py index cc891234616..ddaced5ad0c 100644 --- a/hardware-testing/hardware_testing/gravimetric/helpers.py +++ b/hardware-testing/hardware_testing/gravimetric/helpers.py @@ -1,5 +1,6 @@ """Opentrons helper methods.""" import asyncio +from random import random, randint from types import MethodType from typing import Any, List, Dict, Optional, Tuple from statistics import stdev @@ -320,10 +321,14 @@ def _get_volumes( test_volumes = get_volume_increments( pipette_channels, pipette_volume, tip_volume, mode=mode ) - elif user_volumes and not ctx.is_simulating(): - _inp = input( - f'Enter desired volumes for tip{tip_volume}, comma separated (eg: "10,100,1000") :' - ) + elif user_volumes: + if ctx.is_simulating(): + rand_vols = [round(random() * tip_volume, 1) for _ in range(randint(1, 3))] + _inp = ",".join([str(r) for r in rand_vols]) + else: + _inp = input( + f'Enter desired volumes for tip{tip_volume}, comma separated (eg: "10,100,1000") :' + ) test_volumes = [ float(vol_str) for vol_str in _inp.strip().split(",") if vol_str ] @@ -347,6 +352,7 @@ def _load_pipette( pipette_volume: int, pipette_mount: str, increment: bool, + photometric: bool, gantry_speed: Optional[int] = None, ) -> InstrumentContext: pip_name = f"flex_{pipette_channels}channel_{pipette_volume}" @@ -367,11 +373,10 @@ def _load_pipette( # NOTE: 8ch QC testing means testing 1 channel at a time, # so we need to decrease the pick-up current to work with 1 tip. - if pipette.channels == 8 and not increment: + if pipette.channels == 8 and not increment and not photometric: hwapi = get_sync_hw_api(ctx) mnt = OT3Mount.LEFT if pipette_mount == "left" else OT3Mount.RIGHT - hwpipette: Pipette = hwapi.hardware_pipettes[mnt.to_mount()] - hwpipette.pick_up_configurations.current = 0.2 + hwapi.update_nozzle_configuration_for_mount(mnt, "H1", "H1", "H1") return pipette diff --git a/hardware-testing/hardware_testing/gravimetric/increments.py b/hardware-testing/hardware_testing/gravimetric/increments.py index 5bf6b8efd3b..bbe79d0785f 100644 --- a/hardware-testing/hardware_testing/gravimetric/increments.py +++ b/hardware-testing/hardware_testing/gravimetric/increments.py @@ -302,18 +302,24 @@ }, 1000: { "default": [ - 2.000, 3.000, - 4.000, 5.000, - 6.000, 7.000, - 8.000, - 9.000, 10.000, + 15.000, + 20.000, 50.000, + 100.000, + 120.000, 200.000, - 1137.10, + 320.000, + 450.000, + 650.000, + 850.000, + 1000.00, + 1030.00, + 1050.00, + 1075.00, ], }, } diff --git a/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py b/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py index 1bc0145e071..a37f21b1b36 100644 --- a/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py +++ b/hardware-testing/hardware_testing/gravimetric/liquid_class/defaults.py @@ -8,8 +8,9 @@ interpolate, ) -_default_submerge_aspirate_mm = 2.5 -_default_submerge_dispense_mm = 2.5 +_default_submerge_aspirate_mm = 1.5 +_p50_multi_submerge_aspirate_mm = 1.5 +_default_submerge_dispense_mm = 1.5 _default_retract_mm = 5.0 _default_retract_discontinuity = 20 @@ -498,7 +499,7 @@ 50: { # P50 50: { # T50 1: AspirateSettings( # 1uL - z_submerge_depth=_default_submerge_aspirate_mm, + z_submerge_depth=_p50_multi_submerge_aspirate_mm, plunger_acceleration=_default_accel_p50_ul_sec_sec, plunger_flow_rate=35, # ul/sec delay=_default_aspirate_delay_seconds, @@ -508,7 +509,7 @@ trailing_air_gap=0.1, ), 10: AspirateSettings( # 10uL - z_submerge_depth=_default_submerge_aspirate_mm, + z_submerge_depth=_p50_multi_submerge_aspirate_mm, plunger_acceleration=_default_accel_p50_ul_sec_sec, plunger_flow_rate=23.5, # ul/sec delay=_default_aspirate_delay_seconds, @@ -518,7 +519,7 @@ trailing_air_gap=0.1, ), 50: AspirateSettings( # 50uL - z_submerge_depth=_default_submerge_aspirate_mm, + z_submerge_depth=_p50_multi_submerge_aspirate_mm, plunger_acceleration=_default_accel_p50_ul_sec_sec, plunger_flow_rate=35, # ul/sec delay=_default_aspirate_delay_seconds, diff --git a/hardware-testing/hardware_testing/gravimetric/measurement/scale.py b/hardware-testing/hardware_testing/gravimetric/measurement/scale.py index e194a9a42b5..8514590d1b0 100644 --- a/hardware-testing/hardware_testing/gravimetric/measurement/scale.py +++ b/hardware-testing/hardware_testing/gravimetric/measurement/scale.py @@ -15,6 +15,9 @@ RadwagAmbiant, ) +from hardware_testing.data import ui +from serial.tools.list_ports import comports # type: ignore[import] + @dataclass class ScaleConfig: @@ -67,6 +70,21 @@ def build(cls, simulate: bool) -> "Scale": @classmethod def find_port(cls) -> str: """Find port.""" + ports = comports() + assert ports + for port in ports: + try: + ui.print_info(f"Checking port {port.device} for scale") + radwag = Scale(scale=RadwagScale.create(port.device)) + radwag.connect() + radwag.initialize() + scale_serial = radwag.read_serial_number() + radwag.disconnect() + ui.print_info(f"found scale {scale_serial} on port {port.device}") + return port.device + except: # noqa: E722 + pass + ui.print_info("Unable to find the scale: please connect") return list_ports_and_select(device_name="scale") @property diff --git a/hardware-testing/hardware_testing/gravimetric/overrides/api.patch b/hardware-testing/hardware_testing/gravimetric/overrides/api.patch index 07b69b55ec7..9f143e52892 100644 --- a/hardware-testing/hardware_testing/gravimetric/overrides/api.patch +++ b/hardware-testing/hardware_testing/gravimetric/overrides/api.patch @@ -1,8 +1,8 @@ diff --git a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py -index 174a8f76e4..01b81cd6a0 100644 +index 1f6dd0b4b5..1d0cb7b7e3 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/pipette.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/pipette.py -@@ -456,11 +456,11 @@ class Pipette(AbstractInstrument[PipetteConfigurations]): +@@ -432,11 +432,11 @@ class Pipette(AbstractInstrument[PipetteConfigurations]): def set_current_volume(self, new_volume: float) -> None: assert new_volume >= 0 @@ -16,7 +16,7 @@ index 174a8f76e4..01b81cd6a0 100644 self._current_volume += volume_incr def remove_current_volume(self, volume_incr: float) -> None: -@@ -468,7 +468,8 @@ class Pipette(AbstractInstrument[PipetteConfigurations]): +@@ -444,7 +444,8 @@ class Pipette(AbstractInstrument[PipetteConfigurations]): self._current_volume -= volume_incr def ok_to_add_volume(self, volume_incr: float) -> bool: @@ -27,10 +27,10 @@ index 174a8f76e4..01b81cd6a0 100644 def ok_to_push_out(self, push_out_dist_mm: float) -> bool: return push_out_dist_mm <= ( diff --git a/api/src/opentrons/protocol_api/core/legacy/deck.py b/api/src/opentrons/protocol_api/core/legacy/deck.py -index b0ca38d294..f213febabd 100644 +index ea4068934b..1b21cac251 100644 --- a/api/src/opentrons/protocol_api/core/legacy/deck.py +++ b/api/src/opentrons/protocol_api/core/legacy/deck.py -@@ -47,11 +47,11 @@ class DeckItem(Protocol): +@@ -48,11 +48,11 @@ class DeckItem(Protocol): class Deck(UserDict): # type: ignore[type-arg] data: Dict[int, Optional[DeckItem]] @@ -47,7 +47,7 @@ index b0ca38d294..f213febabd 100644 for slot in self._definition["locations"]["orderedSlots"]: self.data[int(slot["id"])] = None diff --git a/api/src/opentrons/protocol_api/create_protocol_context.py b/api/src/opentrons/protocol_api/create_protocol_context.py -index f2d8e492ec..dd4fd9102e 100644 +index 5a64e70cf9..7d5047cc4b 100644 --- a/api/src/opentrons/protocol_api/create_protocol_context.py +++ b/api/src/opentrons/protocol_api/create_protocol_context.py @@ -22,6 +22,7 @@ from .deck import Deck diff --git a/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch b/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch index b2d08d109e9..c7243e0d27a 100644 --- a/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch +++ b/hardware-testing/hardware_testing/gravimetric/overrides/shared-data.patch @@ -870,3 +870,183 @@ index 0000000000..8ad4397cba + } + ] +} +diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json +index f89fb178b5..5cd8acd638 100644 +--- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json ++++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/default/3_5.json +@@ -20,50 +20,50 @@ + "aspirate": { + "default": { + "1": [ +- [0.462, 0.5646, 0.0415], +- [0.648, 0.3716, 0.1307], +- [1.032, 0.2742, 0.1938], +- [1.37, 0.1499, 0.3221], +- [2.014, 0.1044, 0.3845], +- [2.772, 0.0432, 0.5076], +- [3.05, -0.0809, 0.8517], +- [3.4, 0.0256, 0.5268], +- [3.962, 0.0612, 0.4057], +- [4.438, 0.0572, 0.4217], +- [5.164, 0.018, 0.5955], +- [5.966, 0.0095, 0.6393], +- [7.38, 0.0075, 0.6514], +- [9.128, 0.0049, 0.6705], +- [10.16, 0.0033, 0.6854], +- [13.812, 0.0024, 0.6948], +- [27.204, 0.0008, 0.7165], +- [50.614, 0.0002, 0.7328], +- [53.046, -0.0005, 0.7676] ++ [0.3100,0.5910,0.0197], ++ [0.3900,0.2586,0.1227], ++ [0.8600,0.3697,0.0794], ++ [1.2900,0.2310,0.1987], ++ [1.9300,0.1144,0.3491], ++ [2.7000,0.0536,0.4664], ++ [2.9500,-0.1041,0.8923], ++ [3.2800,0.0216,0.5214], ++ [3.7600,0.0480,0.4349], ++ [4.3800,0.0830,0.3032], ++ [5.0800,0.0153,0.5996], ++ [5.9000,0.0136,0.6083], ++ [7.2900,0.0070,0.6474], ++ [9.0400,0.0059,0.6551], ++ [10.0800,0.0045,0.6682], ++ [13.7400,0.0029,0.6842], ++ [27.1500,0.0010,0.7104], ++ [50.4800,0.0002,0.7319], ++ [52.8900,-0.0006,0.7703] + ] + } + }, + "dispense": { + "default": { + "1": [ +- [0.462, 0.5646, 0.0415], +- [0.648, 0.3716, 0.1307], +- [1.032, 0.2742, 0.1938], +- [1.37, 0.1499, 0.3221], +- [2.014, 0.1044, 0.3845], +- [2.772, 0.0432, 0.5076], +- [3.05, -0.0809, 0.8517], +- [3.4, 0.0256, 0.5268], +- [3.962, 0.0612, 0.4057], +- [4.438, 0.0572, 0.4217], +- [5.164, 0.018, 0.5955], +- [5.966, 0.0095, 0.6393], +- [7.38, 0.0075, 0.6514], +- [9.128, 0.0049, 0.6705], +- [10.16, 0.0033, 0.6854], +- [13.812, 0.0024, 0.6948], +- [27.204, 0.0008, 0.7165], +- [50.614, 0.0002, 0.7328], +- [53.046, -0.0005, 0.7676] ++ [0.3100,0.5910,0.0197], ++ [0.3900,0.2586,0.1227], ++ [0.8600,0.3697,0.0794], ++ [1.2900,0.2310,0.1987], ++ [1.9300,0.1144,0.3491], ++ [2.7000,0.0536,0.4664], ++ [2.9500,-0.1041,0.8923], ++ [3.2800,0.0216,0.5214], ++ [3.7600,0.0480,0.4349], ++ [4.3800,0.0830,0.3032], ++ [5.0800,0.0153,0.5996], ++ [5.9000,0.0136,0.6083], ++ [7.2900,0.0070,0.6474], ++ [9.0400,0.0059,0.6551], ++ [10.0800,0.0045,0.6682], ++ [13.7400,0.0029,0.6842], ++ [27.1500,0.0010,0.7104], ++ [50.4800,0.0002,0.7319], ++ [52.8900,-0.0006,0.7703] + ] + } + }, +diff --git a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json +index e925e4e401..603a2cf861 100644 +--- a/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json ++++ b/shared-data/pipette/definitions/2/liquid/single_channel/p50/lowVolumeDefault/3_5.json +@@ -20,46 +20,48 @@ + "aspirate": { + "default": { + "1": [ +- [0.11, 0.207815, 0.040201], +- [0.65, 0.43933, 0.014735], +- [1.04, 0.256666, 0.133466], +- [1.67, 0.147126, 0.247388], +- [2.45, 0.078774, 0.361536], +- [2.89, 0.042387, 0.450684], +- [3.2, 0.014781, 0.530464], +- [3.79, 0.071819, 0.347944], +- [4.22, 0.051592, 0.424605], +- [4.93, 0.021219, 0.552775], +- [5.81, 0.023461, 0.541725], +- [7.21, 0.008959, 0.625982], +- [8.93, 0.005456, 0.651235], +- [10.0, 0.007108, 0.636489], +- [13.61, 0.002591, 0.681656], +- [26.99, 0.001163, 0.701094], +- [45.25, 0.000207, 0.726887] ++ [0.3000,0.4590,0.0586], ++ [0.4700,0.4300,0.0674], ++ [0.9000,0.3404,0.1095], ++ [1.2600,0.1925,0.2425], ++ [1.9500,0.1314,0.3195], ++ [2.7600,0.0604,0.4580], ++ [2.9500,-0.2085,1.2002], ++ [3.3300,0.0425,0.4597], ++ [3.8700,0.0592,0.4040], ++ [4.3100,0.0518,0.4327], ++ [5.0700,0.0264,0.5424], ++ [5.9300,0.0186,0.5818], ++ [7.3400,0.0078,0.6458], ++ [9.0800,0.0050,0.6664], ++ [10.0900,0.0022,0.6918], ++ [13.7400,0.0027,0.6868], ++ [27.1300,0.0009,0.7109], ++ [45.4300,-0.0038,0.8391] + ] + } + }, + "dispense": { + "default": { + "1": [ +- [0.11, 0.207815, 0.040201], +- [0.65, 0.43933, 0.014735], +- [1.04, 0.256666, 0.133466], +- [1.67, 0.147126, 0.247388], +- [2.45, 0.078774, 0.361536], +- [2.89, 0.042387, 0.450684], +- [3.2, 0.014781, 0.530464], +- [3.79, 0.071819, 0.347944], +- [4.22, 0.051592, 0.424605], +- [4.93, 0.021219, 0.552775], +- [5.81, 0.023461, 0.541725], +- [7.21, 0.008959, 0.625982], +- [8.93, 0.005456, 0.651235], +- [10.0, 0.007108, 0.636489], +- [13.61, 0.002591, 0.681656], +- [26.99, 0.001163, 0.701094], +- [45.25, 0.000207, 0.726887] ++ [0.3000,0.4590,0.0586], ++ [0.4700,0.4300,0.0674], ++ [0.9000,0.3404,0.1095], ++ [1.2600,0.1925,0.2425], ++ [1.9500,0.1314,0.3195], ++ [2.7600,0.0604,0.4580], ++ [2.9500,-0.2085,1.2002], ++ [3.3300,0.0425,0.4597], ++ [3.8700,0.0592,0.4040], ++ [4.3100,0.0518,0.4327], ++ [5.0700,0.0264,0.5424], ++ [5.9300,0.0186,0.5818], ++ [7.3400,0.0078,0.6458], ++ [9.0800,0.0050,0.6664], ++ [10.0900,0.0022,0.6918], ++ [13.7400,0.0027,0.6868], ++ [27.1300,0.0009,0.7109], ++ [45.4300,-0.0038,0.8391] + ] + } + }, diff --git a/hardware-testing/hardware_testing/gravimetric/trial.py b/hardware-testing/hardware_testing/gravimetric/trial.py index 72d370930c2..9c410b5d1eb 100644 --- a/hardware-testing/hardware_testing/gravimetric/trial.py +++ b/hardware-testing/hardware_testing/gravimetric/trial.py @@ -146,7 +146,7 @@ def build_gravimetric_trials( for trial in range(cfg.trials): d: Optional[float] = None cv: Optional[float] = None - if not cfg.increment: + if not cfg.increment and not cfg.user_volumes: d, cv = config.QC_TEST_MIN_REQUIREMENTS[cfg.pipette_channels][ cfg.pipette_volume ][cfg.tip_volume][volume] diff --git a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py index 9f7d0215af4..cd442e3e170 100644 --- a/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py +++ b/hardware-testing/hardware_testing/opentrons_api/helpers_ot3.py @@ -4,10 +4,10 @@ from datetime import datetime from enum import Enum from math import pi -from subprocess import run +from subprocess import run, Popen from time import time from typing import Callable, Coroutine, Dict, List, Optional, Tuple, Union - +import atexit from opentrons_hardware.drivers.can_bus import DriverSettings, build, CanMessenger from opentrons_hardware.drivers.can_bus import settings as can_bus_settings from opentrons_hardware.firmware_bindings.constants import SensorId @@ -77,6 +77,15 @@ def stop_server_ot3() -> None: """Stop opentrons-robot-server on the OT3.""" print('Stopping "opentrons-robot-server"...') run(["systemctl", "stop", "opentrons-robot-server"]) + atexit.register(restart_server_ot3) + + +def restart_server_ot3() -> None: + """Start opentrons-robot-server on the OT3.""" + print('Starting "opentrons-robot-server"...') + Popen( + ["systemctl", "restart", "opentrons-robot-server", "&"], + ) def start_server_ot3() -> None: @@ -546,9 +555,11 @@ async def update_pick_up_current( ) -> None: """Update pick-up-tip current.""" pipette = _get_pipette_from_mount(api, mount) - config_model = pipette.pick_up_configurations - config_model.current = current - pipette.pick_up_configurations = config_model + config_model = pipette.pick_up_configurations.press_fit + config_model.current_by_tip_count = { + k: current for k in config_model.current_by_tip_count.keys() + } + pipette.pick_up_configurations.press_fit = config_model async def update_pick_up_distance( @@ -556,9 +567,9 @@ async def update_pick_up_distance( ) -> None: """Update pick-up-tip current.""" pipette = _get_pipette_from_mount(api, mount) - config_model = pipette.pick_up_configurations + config_model = pipette.pick_up_configurations.press_fit config_model.distance = distance - pipette.pick_up_configurations = config_model + pipette.pick_up_configurations.press_fit = config_model async def move_plunger_absolute_ot3( diff --git a/hardware-testing/hardware_testing/production_qc/gripper_assembly_qc_ot3/test_width.py b/hardware-testing/hardware_testing/production_qc/gripper_assembly_qc_ot3/test_width.py index 7813a9e9340..453b038313b 100644 --- a/hardware-testing/hardware_testing/production_qc/gripper_assembly_qc_ot3/test_width.py +++ b/hardware-testing/hardware_testing/production_qc/gripper_assembly_qc_ot3/test_width.py @@ -16,8 +16,8 @@ from hardware_testing.opentrons_api.types import Axis, OT3Mount, Point FAILURE_THRESHOLD_MM = -3 -GAUGE_HEIGHT_MM = 40 -GRIP_HEIGHT_MM = 30 +GAUGE_HEIGHT_MM = 75 +GRIP_HEIGHT_MM = 48 TEST_WIDTHS_MM: List[float] = [60, 85.75, 62] SLOT_WIDTH_GAUGE: List[Optional[int]] = [None, 3, 9] GRIP_FORCES_NEWTON: List[float] = [10, 15, 20] diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/__main__.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/__main__.py index 441c016cee9..7495e9f5d2c 100644 --- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/__main__.py +++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/__main__.py @@ -31,7 +31,7 @@ async def _main(cfg: TestConfig) -> None: await api.home() home_pos = await api.gantry_position(mount) attach_pos = helpers_ot3.get_slot_calibration_square_position_ot3(5) - attach_pos = attach_pos._replace(z=home_pos.z) + attach_pos = attach_pos._replace(z=home_pos.z - 100) if not api.hardware_pipettes[mount.to_mount()]: # FIXME: Do not home the plunger using the normal home method. # See section below where we use OT3Controller to home it. diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py index 0689e23d492..b781bb57447 100644 --- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py +++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_capacitance.py @@ -225,7 +225,7 @@ async def _probe(distance: float, speed: float) -> float: else: print("skipping deck-pf") - await api.home_z() + await api.home_z(OT3Mount.LEFT) if not api.is_simulator: ui.get_user_ready("REMOVE probe") await api.remove_tip(OT3Mount.LEFT) diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_droplets.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_droplets.py index 81bf72bd432..cb01bd15f04 100644 --- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_droplets.py +++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_droplets.py @@ -182,7 +182,7 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None: # GATHER NOMINAL POSITIONS trash_nominal = get_trash_nominal() tip_rack_96_a1_nominal = get_tiprack_96_nominal() - tip_rack_partial_a1_nominal = get_tiprack_partial_nominal() + # tip_rack_partial_a1_nominal = get_tiprack_partial_nominal() reservoir_a1_nominal = get_reservoir_nominal() reservoir_a1_actual: Optional[Point] = None @@ -223,32 +223,32 @@ async def _find_reservoir_pos() -> None: report(section, "droplets-96-tips", [duration, CSVResult.from_bool(result)]) await _drop_tip(api, trash_nominal) - if not api.is_simulator: - ui.get_user_ready(f"REMOVE 96 tip-rack from slot #{TIP_RACK_96_SLOT}") - ui.get_user_ready(f"ADD partial tip-rack to slot #{TIP_RACK_PARTIAL_SLOT}") - - # SAVE PARTIAL TIP-RACK POSITION - ui.print_header("JOG to Partial-Tip RACK") - await helpers_ot3.move_to_arched_ot3( - api, OT3Mount.LEFT, tip_rack_partial_a1_nominal + Point(z=10) - ) - await helpers_ot3.jog_mount_ot3(api, OT3Mount.LEFT) - tip_rack_partial_a1_actual = await api.gantry_position(OT3Mount.LEFT) - - # TEST PARTIAL-TIP - for test_name, details in PARTIAL_TESTS.items(): - ui.print_header(f"{test_name.upper().replace('-', ' ')}") - pick_up_position = tip_rack_partial_a1_actual + details[0] - await helpers_ot3.move_to_arched_ot3( - api, OT3Mount.LEFT, pick_up_position + Point(z=50) - ) - await _partial_pick_up(api, pick_up_position, current=details[1]) - await _find_reservoir_pos() - assert reservoir_a1_actual - result, duration = await aspirate_and_wait( - api, reservoir_a1_actual, seconds=NUM_SECONDS_TO_WAIT - ) - report( - section, f"droplets-{test_name}", [duration, CSVResult.from_bool(result)] - ) - await _drop_tip(api, trash_nominal) + # if not api.is_simulator: + # ui.get_user_ready(f"REMOVE 96 tip-rack from slot #{TIP_RACK_96_SLOT}") + # ui.get_user_ready(f"ADD partial tip-rack to slot #{TIP_RACK_PARTIAL_SLOT}") + # + # # SAVE PARTIAL TIP-RACK POSITION + # ui.print_header("JOG to Partial-Tip RACK") + # await helpers_ot3.move_to_arched_ot3( + # api, OT3Mount.LEFT, tip_rack_partial_a1_nominal + Point(z=10) + # ) + # await helpers_ot3.jog_mount_ot3(api, OT3Mount.LEFT) + # tip_rack_partial_a1_actual = await api.gantry_position(OT3Mount.LEFT) + # + # # TEST PARTIAL-TIP + # for test_name, details in PARTIAL_TESTS.items(): + # ui.print_header(f"{test_name.upper().replace('-', ' ')}") + # pick_up_position = tip_rack_partial_a1_actual + details[0] + # await helpers_ot3.move_to_arched_ot3( + # api, OT3Mount.LEFT, pick_up_position + Point(z=50) + # ) + # await _partial_pick_up(api, pick_up_position, current=details[1]) + # await _find_reservoir_pos() + # assert reservoir_a1_actual + # result, duration = await aspirate_and_wait( + # api, reservoir_a1_actual, seconds=NUM_SECONDS_TO_WAIT + # ) + # report( + # section, f"droplets-{test_name}", [duration, CSVResult.from_bool(result)] + # ) + # await _drop_tip(api, trash_nominal) diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_jaws.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_jaws.py index 4531fd08007..a6298dd758b 100644 --- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_jaws.py +++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_jaws.py @@ -13,12 +13,18 @@ from hardware_testing.opentrons_api import helpers_ot3 from hardware_testing.opentrons_api.types import Axis, OT3Mount -RETRACT_MM = 0.25 +# from opentrons.hardware_control.backends.ot3utils import axis_convert + + +RETRACT_MM = 0.25 # 0.25 MAX_TRAVEL = 29.8 - RETRACT_MM # FIXME: what is the max travel? -ENDSTOP_OVERRUN_MM = 0.25 +ENDSTOP_OVERRUN_MM = ( + 0.25 # FIXME: position cannot go negative, can't go past limit switch +) ENDSTOP_OVERRUN_SPEED = 5 -SPEEDS_TO_TEST: List[float] = [3, 6, 9, 12, 15] +SPEEDS_TO_TEST: List[float] = [8, 12] CURRENTS_SPEEDS: Dict[float, List[float]] = { + 0.7: SPEEDS_TO_TEST, 1.5: SPEEDS_TO_TEST, } @@ -35,29 +41,61 @@ def build_csv_lines() -> List[Union[CSVLine, CSVLineRepeating]]: speeds = CURRENTS_SPEEDS[current] for speed in sorted(speeds): tag = _get_test_tag(current, speed) - lines.append(CSVLine(tag, [bool, bool, CSVResult])) + lines.append(CSVLine(tag, [bool, bool, bool, CSVResult])) return lines -async def _check_if_jaw_is_aligned_with_endstop(api: OT3API) -> Tuple[bool, bool]: +async def _check_if_jaw_is_aligned_with_endstop(api: OT3API) -> bool: if not api.is_simulator: - pass_no_hit = ui.get_user_answer("are both endstop Lights OFF") + pass_no_hit = ui.get_user_answer("are both endstop Lights OFF?") else: pass_no_hit = True if not pass_no_hit: ui.print_error("endstop hit too early") - return pass_no_hit, False - # now purposefully hit the endstop - await helpers_ot3.move_tip_motor_relative_ot3( - api, -RETRACT_MM - ENDSTOP_OVERRUN_MM, speed=ENDSTOP_OVERRUN_SPEED - ) + + return pass_no_hit + + # This currently does not work since jaws cannot move above 0 + # # now purposefully hit the endstop + # await helpers_ot3.move_tip_motor_relative_ot3( + # api, -RETRACT_MM-ENDSTOP_OVERRUN_MM, speed=ENDSTOP_OVERRUN_SPEED + # ) + # print(await api.get_limit_switches()) + # if not api.is_simulator: + # pass_hit = ui.get_user_answer("are both endstop Lights ON?") + # else: + # pass_hit = True + # if not pass_hit: + # ui.print_error("endstop did not hit") + # return pass_no_hit, pass_hit + + +async def jaw_precheck(api: OT3API, ax: Axis, speed: float) -> Tuple[bool, bool]: + """Check the LEDs work and jaws are aligned.""" + # HOME + print("homing...") + await api.home([ax]) + # Check LEDs can turn on when homed if not api.is_simulator: - pass_hit = ui.get_user_answer("are both endstop Lights ON") + led_check = ui.get_user_answer("are both endstop Lights ON?") else: - pass_hit = True - if not pass_hit: - ui.print_error("endstop did not hit") - return pass_no_hit, pass_hit + led_check = True + if not led_check: + ui.print_error("Endstop LED or homing failure") + return (led_check, False) + + print(f"retracting {RETRACT_MM} mm") + await helpers_ot3.move_tip_motor_relative_ot3(api, RETRACT_MM, speed=speed) + # Check Jaws are aligned + if not api.is_simulator: + jaws_aligned = ui.get_user_answer("are both endstop Lights OFF?") + else: + jaws_aligned = True + + if not jaws_aligned: + ui.print_error("Jaws Misaligned") + + return led_check, jaws_aligned async def run(api: OT3API, report: CSVReport, section: str) -> None: @@ -67,11 +105,14 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None: default_current = settings.run_current default_speed = settings.max_speed - async def _save_result(tag: str) -> bool: - no_hit, hit = await _check_if_jaw_is_aligned_with_endstop(api) - result = CSVResult.from_bool(no_hit and hit) - report(section, tag, [no_hit, hit, result]) - return no_hit and hit + async def _save_result(tag: str, led_check: bool, jaws_aligned: bool) -> bool: + if led_check and jaws_aligned: + no_hit = await _check_if_jaw_is_aligned_with_endstop(api) + else: + no_hit = False + result = CSVResult.from_bool(led_check and jaws_aligned and no_hit) + report(section, tag, [led_check, jaws_aligned, no_hit, result]) + return led_check and jaws_aligned and no_hit await api.home_z(OT3Mount.LEFT) slot_5 = helpers_ot3.get_slot_calibration_square_position_ot3(5) @@ -84,37 +125,40 @@ async def _save_result(tag: str) -> bool: speeds = CURRENTS_SPEEDS[current] for speed in sorted(speeds, reverse=False): ui.print_header(f"CURRENT: {current}, SPEED: {speed}") - # HOME - print("homing...") - await api.home([ax]) - print(f"retracting {RETRACT_MM} mm") - await helpers_ot3.move_tip_motor_relative_ot3(api, RETRACT_MM, speed=speed) - print(f"lowering run-current to {current} amps") - await helpers_ot3.set_gantry_load_per_axis_motion_settings_ot3( - api, ax, default_max_speed=speed - ) - await helpers_ot3.set_gantry_load_per_axis_current_settings_ot3( - api, ax, run_current=current - ) - # MOVE DOWN then UP - print(f"moving down/up {MAX_TRAVEL} mm at {speed} mm/sec") - await helpers_ot3.move_tip_motor_relative_ot3( - api, MAX_TRAVEL, speed=speed, motor_current=current - ) - await helpers_ot3.move_tip_motor_relative_ot3( - api, -MAX_TRAVEL, speed=speed, motor_current=current - ) - # RESET CURRENTS, CHECK, then HOME - await helpers_ot3.set_gantry_load_per_axis_motion_settings_ot3( - api, ax, default_max_speed=default_speed - ) - await helpers_ot3.set_gantry_load_per_axis_current_settings_ot3( - api, ax, run_current=default_current + + led_check, jaws_aligned = await jaw_precheck(api, ax, speed) + + if led_check and jaws_aligned: + print(f"lowering run-current to {current} amps") + await helpers_ot3.set_gantry_load_per_axis_motion_settings_ot3( + api, ax, default_max_speed=speed + ) + await helpers_ot3.set_gantry_load_per_axis_current_settings_ot3( + api, ax, run_current=current + ) + # MOVE DOWN then UP + print(f"moving down/up {MAX_TRAVEL} mm at {speed} mm/sec") + await helpers_ot3.move_tip_motor_relative_ot3( + api, MAX_TRAVEL, speed=speed, motor_current=current + ) + await helpers_ot3.move_tip_motor_relative_ot3( + api, -MAX_TRAVEL, speed=speed, motor_current=current + ) + # RESET CURRENTS, CHECK + await helpers_ot3.set_gantry_load_per_axis_motion_settings_ot3( + api, ax, default_max_speed=default_speed + ) + await helpers_ot3.set_gantry_load_per_axis_current_settings_ot3( + api, ax, run_current=default_current + ) + passed = await _save_result( + _get_test_tag(current, speed), led_check, jaws_aligned ) - passed = await _save_result(_get_test_tag(current, speed)) - print("homing...") - await api.home([ax]) + if not passed and not api.is_simulator: print(f"current {current} failed") print("skipping any remaining speeds at this current") break + + print("homing...") + await api.home([ax]) diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_plunger.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_plunger.py index 94e12df49ce..1f802e47599 100644 --- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_plunger.py +++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_plunger.py @@ -14,9 +14,11 @@ from hardware_testing.opentrons_api.types import Axis, OT3Mount PLUNGER_MAX_SKIP_MM = 0.1 -SPEEDS_TO_TEST: List[float] = [5, 8, 12, 16, 20] +SPEEDS_TO_TEST: List[float] = [5, 15, 22] CURRENTS_SPEEDS: Dict[float, List[float]] = { - 2.2: SPEEDS_TO_TEST, + 0.6: SPEEDS_TO_TEST, + 0.7: SPEEDS_TO_TEST, + 0.8: SPEEDS_TO_TEST, } diff --git a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_pressure.py b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_pressure.py index 66cd0ecede7..cca8ab3a42d 100644 --- a/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_pressure.py +++ b/hardware-testing/hardware_testing/production_qc/ninety_six_assembly_qc_ot3/test_pressure.py @@ -24,6 +24,25 @@ ASPIRATE_VOLUME = 2 PRESSURE_READINGS = ["open-pa", "sealed-pa", "aspirate-pa", "dispense-pa"] +THRESHOLDS = { + "open-pa": ( + -10, + 10, + ), + "sealed-pa": ( + -30, + 30, + ), + "aspirate-pa": ( + -600, + -400, + ), + "dispense-pa": ( + 2500, + 3500, + ), +} + def _get_test_tag(probe: InstrumentProbeType, reading: str) -> str: assert reading in PRESSURE_READINGS, f"{reading} not in PRESSURE_READINGS" @@ -64,6 +83,17 @@ async def _read_from_sensor( return sum(readings) / num_readings +def check_value(test_value: float, test_name: str) -> CSVResult: + """Determine if value is within pass limits.""" + low_limit = THRESHOLDS[test_name][0] + high_limit = THRESHOLDS[test_name][1] + + if low_limit < test_value and test_value < high_limit: + return CSVResult.PASS + else: + return CSVResult.FAIL + + async def run(api: OT3API, report: CSVReport, section: str) -> None: """Run.""" await api.home_z(OT3Mount.LEFT) @@ -84,8 +114,8 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None: ui.print_error(f"{probe} pressure sensor not working, skipping") continue print(f"open-pa: {open_pa}") - # FIXME: create stricter pass/fail criteria - report(section, _get_test_tag(probe, "open-pa"), [open_pa, CSVResult.PASS]) + open_result = check_value(open_pa, "open-pa") + report(section, _get_test_tag(probe, "open-pa"), [open_pa, open_result]) # SEALED-Pa sealed_pa = 0.0 @@ -102,8 +132,8 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None: ui.print_error(f"{probe} pressure sensor not working, skipping") break print(f"sealed-pa: {sealed_pa}") - # FIXME: create stricter pass/fail criteria - report(section, _get_test_tag(probe, "sealed-pa"), [sealed_pa, CSVResult.PASS]) + sealed_result = check_value(sealed_pa, "sealed-pa") + report(section, _get_test_tag(probe, "sealed-pa"), [sealed_pa, sealed_result]) # ASPIRATE-Pa aspirate_pa = 0.0 @@ -117,9 +147,9 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None: ui.print_error(f"{probe} pressure sensor not working, skipping") break print(f"aspirate-pa: {aspirate_pa}") - # FIXME: create stricter pass/fail criteria + aspirate_result = check_value(aspirate_pa, "aspirate-pa") report( - section, _get_test_tag(probe, "aspirate-pa"), [aspirate_pa, CSVResult.PASS] + section, _get_test_tag(probe, "aspirate-pa"), [aspirate_pa, aspirate_result] ) # DISPENSE-Pa @@ -134,9 +164,9 @@ async def run(api: OT3API, report: CSVReport, section: str) -> None: ui.print_error(f"{probe} pressure sensor not working, skipping") break print(f"dispense-pa: {dispense_pa}") - # FIXME: create stricter pass/fail criteria + dispense_result = check_value(dispense_pa, "dispense-pa") report( - section, _get_test_tag(probe, "dispense-pa"), [dispense_pa, CSVResult.PASS] + section, _get_test_tag(probe, "dispense-pa"), [dispense_pa, dispense_result] ) if not api.is_simulator: diff --git a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py index 656a8387456..f9f60173eed 100644 --- a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py +++ b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py @@ -34,10 +34,9 @@ ) from hardware_testing import data -from hardware_testing.drivers import list_ports_and_select from hardware_testing.drivers.pressure_fixture import ( - PressureFixture, - SimPressureFixture, + PressureFixtureBase, + connect_to_fixture, ) from .pressure import ( # type: ignore[import] PRESSURE_FIXTURE_TIP_VOLUME, @@ -468,15 +467,10 @@ async def _aspirate_and_look_for_droplets( return leak_test_passed -def _connect_to_fixture(test_config: TestConfig) -> PressureFixture: - if not test_config.simulate and not test_config.skip_fixture: - if not test_config.fixture_port: - _port = list_ports_and_select("pressure-fixture") - else: - _port = "" - fixture = PressureFixture.create(port=_port, slot_side=test_config.fixture_side) - else: - fixture = SimPressureFixture() # type: ignore[assignment] +def _connect_to_fixture(test_config: TestConfig) -> PressureFixtureBase: + fixture = connect_to_fixture( + test_config.simulate or test_config.skip_fixture, side=test_config.fixture_side + ) fixture.connect() return fixture @@ -485,7 +479,7 @@ async def _read_pressure_and_check_results( api: OT3API, pipette_channels: int, pipette_volume: int, - fixture: PressureFixture, + fixture: PressureFixtureBase, tag: PressureEvent, write_cb: Callable, accumulate_raw_data_cb: Callable, @@ -599,7 +593,7 @@ async def _fixture_check_pressure( api: OT3API, mount: OT3Mount, test_config: TestConfig, - fixture: PressureFixture, + fixture: PressureFixtureBase, write_cb: Callable, accumulate_raw_data_cb: Callable, ) -> bool: @@ -694,7 +688,7 @@ async def _test_for_leak( mount: OT3Mount, test_config: TestConfig, tip_volume: int, - fixture: Optional[PressureFixture], + fixture: Optional[PressureFixtureBase], write_cb: Optional[Callable], accumulate_raw_data_cb: Optional[Callable], droplet_wait_seconds: int = 30, diff --git a/hardware-testing/hardware_testing/production_qc/tip_iqc_ot3.py b/hardware-testing/hardware_testing/production_qc/tip_iqc_ot3.py index 65f11196534..042443c4d32 100644 --- a/hardware-testing/hardware_testing/production_qc/tip_iqc_ot3.py +++ b/hardware-testing/hardware_testing/production_qc/tip_iqc_ot3.py @@ -1,13 +1,13 @@ """Tip IQC OT3.""" from asyncio import run, sleep -from typing import List, Union, Optional +from typing import List, Optional from opentrons.hardware_control.ot3api import OT3API -from hardware_testing.drivers import list_ports_and_select from hardware_testing.drivers.pressure_fixture import ( - PressureFixture, + PressureFixtureBase, SimPressureFixture, + connect_to_fixture, ) from hardware_testing.data.csv_report import CSVReport, CSVSection, CSVLine @@ -44,18 +44,14 @@ async def _find_position(api: OT3API, mount: OT3Mount, nominal: Point) -> Point: return await api.gantry_position(mount) -def _connect_to_fixture(simulate: bool) -> PressureFixture: - if not simulate: - _port = list_ports_and_select("pressure-fixture") - fixture = PressureFixture.create(port=_port, slot_side="left") - else: - fixture = SimPressureFixture() # type: ignore[assignment] +def _connect_to_fixture(simulate: bool) -> PressureFixtureBase: + fixture = connect_to_fixture(simulate) # type: ignore[assignment] fixture.connect() return fixture async def _read_pressure_data( - fixture: Union[PressureFixture, SimPressureFixture], + fixture: PressureFixtureBase, num_samples: int, interval: float = DEFAULT_PRESSURE_FIXTURE_READ_INTERVAL_SECONDS, ) -> List[float]: @@ -73,7 +69,7 @@ async def _read_and_store_pressure_data( report: CSVReport, tip: str, section: str, - fixture: Union[PressureFixture, SimPressureFixture], + fixture: PressureFixtureBase, ) -> None: num_samples = TEST_SECTIONS[section.lower()] data_hover = await _read_pressure_data(fixture, num_samples) diff --git a/hardware-testing/hardware_testing/production_qc/z_stage_qc_ot3.py b/hardware-testing/hardware_testing/production_qc/z_stage_qc_ot3.py index dd454177710..7806561568a 100644 --- a/hardware-testing/hardware_testing/production_qc/z_stage_qc_ot3.py +++ b/hardware-testing/hardware_testing/production_qc/z_stage_qc_ot3.py @@ -223,9 +223,9 @@ async def _force_gauge( await api.home([z_ax]) home_pos = await api.gantry_position(mount) LOG.info(f"Home Position: {home_pos}") - pre_test_pos = home_pos._replace(z=home_pos.z - 110) + pre_test_pos = home_pos._replace(z=home_pos.z - 15) LOG.info(f"Pre-Test Position: {pre_test_pos}") - press_pos = home_pos._replace(z=pre_test_pos.z - 113) + press_pos = home_pos._replace(z=pre_test_pos.z - 30) LOG.info(f"Press Position: {press_pos}") qc_pass = True diff --git a/hardware-testing/hardware_testing/scripts/gripper_move.py b/hardware-testing/hardware_testing/scripts/gripper_move.py new file mode 100644 index 00000000000..1b50f6cfb6f --- /dev/null +++ b/hardware-testing/hardware_testing/scripts/gripper_move.py @@ -0,0 +1,82 @@ +"""Demo OT3 Gantry Functionality.""" +# Author: Carlos Ferandez None: + hw_api = await build_async_ot3_hardware_api( + is_simulating=args.simulate, use_defaults=True + ) + await asyncio.sleep(1) + await hw_api.cache_instruments() + timeout_start = time.time() + timeout = 60 * 60 * 3 + count = 0 + x_offset = 80 + y_offset = 44 + try: + await hw_api.home() + await asyncio.sleep(1) + await hw_api.set_lights(rails=True) + home_position = await hw_api.current_position_ot3(mount) + await hw_api.grip(force_newtons=None, stay_engaged=True) + print(f"home: {home_position}") + x_home = home_position[Axis.X] - x_offset + y_home = home_position[Axis.Y] - y_offset + z_home = home_position[Axis.Z_G] + while time.time() < timeout_start + timeout: + # while True: + print(f"time: {time.time()-timeout_start}") + await hw_api.move_to(mount, Point(x_home, y_home, z_home)) + await hw_api.move_to(mount, Point(x_home, y_home, z_home - 190)) + count += 1 + print(f"cycle: {count}") + await hw_api.home() + except KeyboardInterrupt: + await hw_api.disengage_axes([Axis.X, Axis.Y, Axis.G]) + finally: + await hw_api.disengage_axes([Axis.X, Axis.Y, Axis.G]) + await hw_api.clean_up() + + +if __name__ == "__main__": + slot_locs = [ + "A1", + "A2", + "A3", + "B1", + "B2", + "B3:", + "C1", + "C2", + "C3", + "D1", + "D2", + "D3", + ] + parser = argparse.ArgumentParser() + parser.add_argument("--simulate", action="store_true") + parser.add_argument("--trough", action="store_true") + parser.add_argument("--tiprack", action="store_true") + parser.add_argument( + "--mount", type=str, choices=["left", "right", "gripper"], default="gripper" + ) + args = parser.parse_args() + if args.mount == "left": + mount = OT3Mount.LEFT + if args.mount == "gripper": + mount = OT3Mount.GRIPPER + else: + mount = OT3Mount.RIGHT + asyncio.run(_main()) diff --git a/hardware-testing/hardware_testing/scripts/tip_pick_up_lifetime_test.py b/hardware-testing/hardware_testing/scripts/tip_pick_up_lifetime_test.py new file mode 100644 index 00000000000..5a449f64998 --- /dev/null +++ b/hardware-testing/hardware_testing/scripts/tip_pick_up_lifetime_test.py @@ -0,0 +1,602 @@ +"""Lifetime test.""" +import argparse +import asyncio +import time + +import os +import sys +import termios +import tty +import json +from typing import Dict, Tuple +from hardware_testing.opentrons_api import types +from hardware_testing.opentrons_api import helpers_ot3 +from hardware_testing import data + +from hardware_testing.opentrons_api.types import OT3Mount, Axis, Point + +from opentrons.hardware_control.types import CriticalPoint +from opentrons.hardware_control.ot3api import OT3API + + +def _convert(seconds: float) -> str: + weeks, seconds = divmod(seconds, 7 * 24 * 60 * 60) + days, seconds = divmod(seconds, 24 * 60 * 60) + hours, seconds = divmod(seconds, 60 * 60) + minutes, seconds = divmod(seconds, 60) + + return "%02d:%02d:%02d:%02d:%02d" % (weeks, days, hours, minutes, seconds) + + +def _getch() -> str: + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(fd) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + + +async def jog( + api: OT3API, position: Dict[Axis, float], cp: CriticalPoint +) -> Dict[Axis, float]: + """Move the gantry.""" + step_size = [0.01, 0.05, 0.1, 0.5, 1, 10, 20, 50] + step_length_index = 3 + xy_speed = 60 + za_speed = 65 + information_str = """ + Click >> i << to move up + Click >> k << to move down + Click >> a << to move left + Click >> d << to move right + Click >> w << to move forward + Click >> s << to move back + Click >> + << to Increase the length of each step + Click >> - << to decrease the length of each step + Click >> Enter << to save position + Click >> q << to quit the test script + """ + print(information_str) + while True: + input = _getch() + if input == "a": + # minus x direction + sys.stdout.flush() + await api.move_rel( + mount, Point(-step_size[step_length_index], 0, 0), speed=xy_speed + ) + + elif input == "d": + # plus x direction + sys.stdout.flush() + await api.move_rel( + mount, Point(step_size[step_length_index], 0, 0), speed=xy_speed + ) + + elif input == "w": + # minus y direction + sys.stdout.flush() + await api.move_rel( + mount, Point(0, step_size[step_length_index], 0), speed=xy_speed + ) + + elif input == "s": + # plus y direction + sys.stdout.flush() + await api.move_rel( + mount, Point(0, -step_size[step_length_index], 0), speed=xy_speed + ) + + elif input == "i": + sys.stdout.flush() + await api.move_rel( + mount, Point(0, 0, step_size[step_length_index]), speed=za_speed + ) + + elif input == "k": + sys.stdout.flush() + await api.move_rel( + mount, Point(0, 0, -step_size[step_length_index]), speed=za_speed + ) + + elif input == "q": + sys.stdout.flush() + print("TEST CANCELLED") + quit() + + elif input == "+": + sys.stdout.flush() + step_length_index = step_length_index + 1 + if step_length_index >= 7: + step_length_index = 7 + + elif input == "-": + sys.stdout.flush() + step_length_index = step_length_index - 1 + if step_length_index <= 0: + step_length_index = 0 + + elif input == "\r": + sys.stdout.flush() + position = await api.current_position_ot3( + mount, refresh=True, critical_point=cp + ) + print("\r\n") + return position + position = await api.current_position_ot3( + mount, refresh=True, critical_point=cp + ) + + print( + "Coordinates: ", + round(position[Axis.X], 2), + ",", + round(position[Axis.Y], 2), + ",", + round(position[Axis.by_mount(mount)], 2), + " Motor Step: ", + step_size[step_length_index], + end="", + ) + print("\r", end="") + + +async def _calibrate_tip_racks( + api: OT3API, + mount: OT3Mount, + slot_loc: Dict[str, Tuple[float, float, int]], + AXIS: Axis, +) -> Dict[str, Tuple[float, float, float]]: + print("Calibrate tip rack positions\n") + calibrated_slot_loc = {} + + for key in slot_loc.keys(): + print(f"TIP RACK IN SLOT {key}\n") + await api.move_to(mount, Point(slot_loc[key][0], slot_loc[key][1], 250.0)) + await api.move_to( + mount, Point(slot_loc[key][0], slot_loc[key][1], slot_loc[key][2]) + ) + # tip_rack_position = await helpers_ot3.jog_mount_ot3(api, mount) + cur_pos = await api.current_position_ot3( + mount, critical_point=CriticalPoint.NOZZLE + ) + tip_rack_position = await jog(api, cur_pos, CriticalPoint.NOZZLE) + calibrated_slot_loc[key] = ( + tip_rack_position[Axis.X], + tip_rack_position[Axis.Y], + tip_rack_position[AXIS], + ) + await api.home([AXIS]) + + json_object = json.dumps(calibrated_slot_loc, indent=0) + # ("/home/root/calibrated_slot_locations.json", "w") + with open("/data/testing_data/calibrated_slot_locations.json", "w") as outfile: + outfile.write(json_object) + return calibrated_slot_loc + + +async def _main(is_simulating: bool, mount: types.OT3Mount) -> None: # noqa: C901 + path = "/data/testing_data/calibrated_slot_locations.json" + api = await helpers_ot3.build_async_ot3_hardware_api(is_simulating=is_simulating) + await api.home() + await api.home_plunger(mount) + + test_tag = "" + test_robot = "Tip Pick Up/Plunger Lifetime" + if args.test_tag: + test_tag = input("Enter test tag:\n\t>> ") + if args.test_robot: + test_robot = input("Enter robot ID:\n\t>> ") + + if mount == OT3Mount.LEFT: + AXIS = Axis.Z_L + else: + AXIS = Axis.Z_R + + # TIP_RACKS = args.tip_rack_num # default: 12 + PICKUPS_PER_TIP = args.pick_up_num # default: 20 + COLUMNS = 12 + ROWS = 8 + CYCLES = 1 + + test_pip = api.get_attached_instrument(mount) + + print("mount.id:{}".format(test_pip["pipette_id"])) + + slot_loc = { + "A1": (13.42, 394.92, 110), + "A2": (177.32, 394.92, 110), + "A3": (341.03, 394.92, 110), + "B1": (13.42, 288.42, 110), + "B2": (177.32, 288.92, 110), + "B3": (341.03, 288.92, 110), + "C1": (13.42, 181.92, 110), + "C2": (177.32, 181.92, 110), + "C3": (341.03, 181.92, 110), + "D1": (13.42, 75.5, 110), + "D2": (177.32, 75.5, 110), + "D3": (341.03, 75.5, 110), + } + + run_id = data.create_run_id() + test_name = "tip-pick-up-lifetime-test" + if args.restart_flag: + if os.path.exists(path): + with open(path, "r") as openfile: + complete_dict = json.load(openfile) + file_name = complete_dict["csv_name"] + else: + print("Slot locations calibration file not found.\n") + calibrated_slot_loc = await _calibrate_tip_racks(api, mount, slot_loc, AXIS) + else: + file_name = data.create_file_name( + test_name=test_name, + run_id=run_id, + tag=test_tag, + ) + header = [ + "Time (W:H:M:S)", + "Test Robot", + "Test Pipette", + "Tip Rack", + "Tip Number", + "Total Tip Pick Ups", + "Tip Presence - Tip Pick Up (P/F)", + "Tip Presence - Tip Eject (P/F)", + "Total Failures", + ] + header_str = data.convert_list_to_csv_line(header) + data.append_data_to_file( + test_name=test_name, run_id=run_id, file_name=file_name, data=header_str + ) + + print("test_pip", test_pip) + if len(test_pip) == 0: + print(f"No pipette recognized on {mount.name} mount\n") + sys.exit() + + print(f"\nTest pipette: {test_pip['name']}\n") + + if "single" in test_pip["name"]: + check_tip_presence = True + if args.pick_up_num == 60: + PICKUPS_PER_TIP = 60 + else: + PICKUPS_PER_TIP = args.pick_up_num + else: + ROWS = 1 + CYCLES = 2 + if args.pick_up_num == 60: + PICKUPS_PER_TIP = 60 + else: + PICKUPS_PER_TIP = args.pick_up_num + check_tip_presence = True + + # just for save calibrate file + if args.only_calibrate: + await _calibrate_tip_racks(api, mount, slot_loc, AXIS) + return + + # optional arg for tip rack calibration + if not args.load_cal: + calibrated_slot_loc = await _calibrate_tip_racks(api, mount, slot_loc, AXIS) + else: + # import calibrated json file + # path = '/home/root/.opentrons/testing_data/calibrated_slot_locations.json' + print("Loading calibration data...\n") + path = "/data/testing_data/calibrated_slot_locations.json" + if os.path.exists(path): + with open( + "/data/testing_data/calibrated_slot_locations.json", "r" + ) as openfile: + calibrated_slot_loc = json.load(openfile) + else: + print("Slot locations calibration file not found.\n") + calibrated_slot_loc = await _calibrate_tip_racks(api, mount, slot_loc, AXIS) + print("Calibration data successfully loaded!\n") + + # add cfg start slot + start_slot = int(str(args.start_slot_row_col_totalTips_totalFailure).split(":")[0]) + start_row = int(str(args.start_slot_row_col_totalTips_totalFailure).split(":")[1]) + start_col = int(str(args.start_slot_row_col_totalTips_totalFailure).split(":")[2]) + total_tip_num = int( + str(args.start_slot_row_col_totalTips_totalFailure).split(":")[3] + ) + total_fail_num = int( + str(args.start_slot_row_col_totalTips_totalFailure).split(":")[4] + ) + + start_time = time.perf_counter() + elapsed_time = 0.0 + rack = start_slot - 1 + total_pick_ups = total_tip_num - 1 + total_failures = total_fail_num + start_tip_nums = 1 + + # load complete information + if args.restart_flag: + if os.path.exists(path): + with open( + "/data/testing_data/calibrated_slot_locations.json", "r" + ) as openfile: + print("load complete information...\n") + load_complete_dict = json.load(openfile) + CYCLES = CYCLES - (load_complete_dict["cycle"] - 1) + rack = load_complete_dict["slot_num"] - 1 + total_pick_ups = load_complete_dict["total_tip_pick_up"] + total_failures = load_complete_dict["total_failure"] + start_slot = rack + 1 + start_row = load_complete_dict["row"] + start_col = load_complete_dict["col"] + start_tip_nums = load_complete_dict["tip_num"] + 1 + else: + print("Failed to load complete information.\n") + + start_slot = start_slot % 12 # fix bug for cycles + for i in range(start_slot - 1): + del calibrated_slot_loc[list(calibrated_slot_loc)[0]] + + for i in range(CYCLES): + print(f"\n=========== Cycle {i + 1}/{CYCLES} ===========\n") + if i > 0: + stop_time = time.perf_counter() + print("Replace tips before continuing test.") + input('\n\t>> Press "Enter" to continue.') + resume_time = time.perf_counter() + elapsed_time += resume_time - stop_time + print(f"Elapsed time: {_convert(resume_time-stop_time)}\n") + for key_index, key in enumerate(calibrated_slot_loc.keys()): + if key_index >= 12: + break + rack += 1 + await api.home([AXIS]) + await api.move_to( + mount, + Point(calibrated_slot_loc[key][0], calibrated_slot_loc[key][1], 250.0), + ) + await api.move_to( + mount, + Point( + calibrated_slot_loc[key][0], + calibrated_slot_loc[key][1], + calibrated_slot_loc[key][2] + 5, + ), + ) + for col in range(COLUMNS): + if col < start_col - 1: + continue + await api.move_to( + mount, + Point( + calibrated_slot_loc[key][0] + 9 * col, + calibrated_slot_loc[key][1], + calibrated_slot_loc[key][2] + 5, + ), + ) + for row in range(ROWS): + if col == start_col - 1 and row < start_row - 1: + continue + print("=================================\n") + print(f"Tip rack in slot {key}, Column: {col+1}, Row: {row+1}\n") + if "p1000" in test_pip["name"]: + if "1" in key: + tip_len = 95.6 + elif "2" in key: + tip_len = 58.35 + elif "3" in key: + tip_len = 57.9 + else: + tip_len = 57.9 + print(f"Tip length: {tip_len} mm\n") + if row > 0: + await api.move_rel(mount, delta=Point(y=-9)) + await api.move_to( + mount, + Point( + calibrated_slot_loc[key][0] + 9 * col, + calibrated_slot_loc[key][1] - 9 * row, + calibrated_slot_loc[key][2] + 5, + ), + ) + await api.move_to( + mount, + Point( + calibrated_slot_loc[key][0] + 9 * col, + calibrated_slot_loc[key][1] - 9 * row, + calibrated_slot_loc[key][2], + ), + ) + start_pos = await api.gantry_position(mount) + for pick_up in range(PICKUPS_PER_TIP): + await api.move_to(mount, start_pos) + if ( + col == start_col - 1 + and row == start_row - 1 + and pick_up < start_tip_nums - 1 + ): + continue + print("= = = = = = = = = = = = = = = = =\n") + print(f"Tip Pick Up #{pick_up+1}\n") + print("Picking up tip...\n") + await api.pick_up_tip(mount, tip_len) + total_pick_ups += 1 + + # check tip presence after tip pick up + + if check_tip_presence: + tip_presence_pick_up = await api.get_tip_presence_status( + mount + ) + # pick_up_keys = list(tip_presence_pick_up.keys()) + if ( + tip_presence_pick_up == 1 + ): # (tip_presence_pick_up[pick_up_keys[0]]): + print("\t>> Tip detected!\n") + tip_presence_pick_up_flag = True + else: + tip_presence_eject = await api.get_tip_presence_status( + mount + ) + print("GET Tip presenc{}".format(tip_presence_eject)) + total_failures += 1 + tip_presence_pick_up_flag = False + print( + f"\t>> Tip not detected! Total failures: {total_failures}\n" + ) + else: + tip_presence_pick_up_flag = False + + # move plunger from blowout to top, back to blow_out + ( + top_pos, + bottom_pos, + _, + _, + ) = helpers_ot3.get_plunger_positions_ot3(api, mount) + + print("Move to bottom plunger position\n") + await helpers_ot3.move_plunger_absolute_ot3( + api, mount, bottom_pos + ) + print("Move to top plunger position\n") + await helpers_ot3.move_plunger_absolute_ot3(api, mount, top_pos) + print("Move to bottom plunger position\n") + await helpers_ot3.move_plunger_absolute_ot3( + api, mount, bottom_pos + ) + + # check tip presence after tip drop + print("Dropping tip...\n") + await api.drop_tip(mount) + if check_tip_presence: + tip_presence_eject = await api.get_tip_presence_status( + mount + ) + # drop_tip_keys = list(tip_presence_eject.keys()) + if ( + tip_presence_eject == 1 + ): # (tip_presence_eject[drop_tip_keys[0]]): + print("GET Tip presenc{}".format(tip_presence_eject)) + print("\t>> Tip detected after ejecting tip!\n") + print("\t>> Canceling script...\n") + total_failures += 1 + tip_presence_eject_flag = True + else: + print("\t>> Tip not detected!\n") + tip_presence_eject_flag = False + else: + tip_presence_eject_flag = False + + # save test data and continue loop/exit based on tip eject success + + cycle_data = [ + _convert(time.perf_counter() - elapsed_time - start_time), + test_robot, + test_pip["pipette_id"], + rack, + pick_up + 1, + total_pick_ups, + tip_presence_pick_up_flag, + tip_presence_eject_flag, + total_failures, + ] + cycle_data_str = data.convert_list_to_csv_line(cycle_data) + data.append_data_to_file( + test_name=test_name, + run_id=run_id, + file_name=file_name, + data=cycle_data_str, + ) + + # save the last complate information + + if os.path.exists(path): + with open( + "/data/testing_data/calibrated_slot_locations.json", "r" + ) as openfile: + print("Recording...\n") + calibrated_slot_loc = json.load(openfile) + complete_dict = { + "cycle": i + 1, + "slot_num": rack, + "tip_num": pick_up + 1, + "total_tip_pick_up": total_pick_ups, + "total_failure": total_failures, + "col": col + 1, + "row": row + 1, + "csv_name": file_name, + } + calibrated_slot_loc.update(complete_dict) + with open( + "/data/testing_data/calibrated_slot_locations.json", + "w", + ) as writefile: + json.dump(calibrated_slot_loc, writefile) + + else: + print("Slot locations calibration file not found.\n") + print("Failed to record complete information.\n") + + if tip_presence_eject_flag: + await api.home() + sys.exit() + + # adjust row increment + print("Moving to next row...\n") + # await api.move_rel(mount, delta=Point(z=5)) + + # adjust column increment + await api.move_to( + mount, + Point( + calibrated_slot_loc[key][0] + 9 * col, + calibrated_slot_loc[key][1] - 9 * row, + calibrated_slot_loc[key][2] + 5, + ), + ) + print("Moving to next column...\n") + + # release start + start_col = 1 + start_row = 1 + start_tip_nums = 1 + + print("=================================\n") + print(f"\nCYCLE {i+1} COMPLETE\n") + await api.home() + await api.home_plunger(mount) + + print("=================================\n") + print("1/4 LIFETIME TEST COMPLETE\n") + await api.home() + + +if __name__ == "__main__": + mount_options = { + "left": types.OT3Mount.LEFT, + "right": types.OT3Mount.RIGHT, + "gripper": types.OT3Mount.GRIPPER, + } + parser = argparse.ArgumentParser() + parser.add_argument("--simulate", action="store_true") + parser.add_argument( + "--mount", type=str, choices=list(mount_options.keys()), default="left" + ) + parser.add_argument("--pick_up_num", type=int, default=60) + # parser.add_argument("--tip_rack_num", type=int, default=12) + parser.add_argument("--load_cal", action="store_true") + parser.add_argument("--test_tag", action="store_true") + parser.add_argument("--test_robot", action="store_true") + parser.add_argument("--restart_flag", action="store_true") + parser.add_argument( + "--start_slot_row_col_totalTips_totalFailure", type=str, default="1:1:1:1:0" + ) + parser.add_argument("--only_calibrate", action="store_true") + # parser.add_argument("--check_tip", action="store_true") + args = parser.parse_args() + mount = mount_options[args.mount] + + asyncio.run(_main(args.simulate, mount)) diff --git a/hardware-testing/hardware_testing/tools/plot/index.html b/hardware-testing/hardware_testing/tools/plot/index.html index 6b8389645be..124607b6a1b 100644 --- a/hardware-testing/hardware_testing/tools/plot/index.html +++ b/hardware-testing/hardware_testing/tools/plot/index.html @@ -5,7 +5,7 @@ Hardware-Testing: Plot - + plotly-logomark"}}},{}],483:[function(t,e,r){"use strict";r.isLeftAnchor=function(t){return"left"===t.xanchor||"auto"===t.xanchor&&t.x<=1/3},r.isCenterAnchor=function(t){return"center"===t.xanchor||"auto"===t.xanchor&&t.x>1/3&&t.x<2/3},r.isRightAnchor=function(t){return"right"===t.xanchor||"auto"===t.xanchor&&t.x>=2/3},r.isTopAnchor=function(t){return"top"===t.yanchor||"auto"===t.yanchor&&t.y>=2/3},r.isMiddleAnchor=function(t){return"middle"===t.yanchor||"auto"===t.yanchor&&t.y>1/3&&t.y<2/3},r.isBottomAnchor=function(t){return"bottom"===t.yanchor||"auto"===t.yanchor&&t.y<=1/3}},{}],484:[function(t,e,r){"use strict";var n=t("./mod"),i=n.mod,a=n.modHalf,o=Math.PI,s=2*o;function l(t){return Math.abs(t[1]-t[0])>s-1e-14}function c(t,e){return a(e-t,s)}function u(t,e){if(l(e))return!0;var r,n;e[0](n=i(n,s))&&(n+=s);var a=i(t,s),o=a+s;return a>=r&&a<=n||o>=r&&o<=n}function f(t,e,r,n,i,a,c){i=i||0,a=a||0;var u,f,h,p,d,m=l([r,n]);function g(t,e){return[t*Math.cos(e)+i,a-t*Math.sin(e)]}m?(u=0,f=o,h=s):r=i&&t<=a);var i,a},pathArc:function(t,e,r,n,i){return f(null,t,e,r,n,i,0)},pathSector:function(t,e,r,n,i){return f(null,t,e,r,n,i,1)},pathAnnulus:function(t,e,r,n,i,a){return f(t,e,r,n,i,a,1)}}},{"./mod":510}],485:[function(t,e,r){"use strict";var n=Array.isArray,i=ArrayBuffer,a=DataView;function o(t){return i.isView(t)&&!(t instanceof a)}function s(t){return n(t)||o(t)}function l(t,e,r){if(s(t)){if(s(t[0])){for(var n=r,i=0;ii.max?e.set(r):e.set(+t)}},integer:{coerceFunction:function(t,e,r,i){t%1||!n(t)||void 0!==i.min&&ti.max?e.set(r):e.set(+t)}},string:{coerceFunction:function(t,e,r,n){if("string"!=typeof t){var i="number"==typeof t;!0!==n.strict&&i?e.set(String(t)):e.set(r)}else n.noBlank&&!t?e.set(r):e.set(t)}},color:{coerceFunction:function(t,e,r){i(t).isValid()?e.set(t):e.set(r)}},colorlist:{coerceFunction:function(t,e,r){Array.isArray(t)&&t.length&&t.every((function(t){return i(t).isValid()}))?e.set(t):e.set(r)}},colorscale:{coerceFunction:function(t,e,r){e.set(o.get(t,r))}},angle:{coerceFunction:function(t,e,r){"auto"===t?e.set("auto"):n(t)?e.set(f(+t,360)):e.set(r)}},subplotid:{coerceFunction:function(t,e,r,n){var i=n.regex||u(r);"string"==typeof t&&i.test(t)?e.set(t):e.set(r)},validateFunction:function(t,e){var r=e.dflt;return t===r||"string"==typeof t&&!!u(r).test(t)}},flaglist:{coerceFunction:function(t,e,r,n){if("string"==typeof t)if(-1===(n.extras||[]).indexOf(t)){for(var i=t.split("+"),a=0;a=n&&t<=i?t:u}if("string"!=typeof t&&"number"!=typeof t)return u;t=String(t);var c=_(e),v=t.charAt(0);!c||"G"!==v&&"g"!==v||(t=t.substr(1),e="");var w=c&&"chinese"===e.substr(0,7),T=t.match(w?x:y);if(!T)return u;var k=T[1],A=T[3]||"1",M=Number(T[5]||1),S=Number(T[7]||0),E=Number(T[9]||0),L=Number(T[11]||0);if(c){if(2===k.length)return u;var C;k=Number(k);try{var P=g.getComponentMethod("calendars","getCal")(e);if(w){var I="i"===A.charAt(A.length-1);A=parseInt(A,10),C=P.newDate(k,P.toMonthIndex(k,A,I),M)}else C=P.newDate(k,Number(A),M)}catch(t){return u}return C?(C.toJD()-m)*f+S*h+E*p+L*d:u}k=2===k.length?(Number(k)+2e3-b)%100+b:Number(k),A-=1;var O=new Date(Date.UTC(2e3,A,M,S,E));return O.setUTCFullYear(k),O.getUTCMonth()!==A||O.getUTCDate()!==M?u:O.getTime()+L*d},n=r.MIN_MS=r.dateTime2ms("-9999"),i=r.MAX_MS=r.dateTime2ms("9999-12-31 23:59:59.9999"),r.isDateTime=function(t,e){return r.dateTime2ms(t,e)!==u};var T=90*f,k=3*h,A=5*p;function M(t,e,r,n,i){if((e||r||n||i)&&(t+=" "+w(e,2)+":"+w(r,2),(n||i)&&(t+=":"+w(n,2),i))){for(var a=4;i%10==0;)a-=1,i/=10;t+="."+w(i,a)}return t}r.ms2DateTime=function(t,e,r){if("number"!=typeof t||!(t>=n&&t<=i))return u;e||(e=0);var a,o,s,c,y,x,b=Math.floor(10*l(t+.05,1)),w=Math.round(t-b/10);if(_(r)){var S=Math.floor(w/f)+m,E=Math.floor(l(t,f));try{a=g.getComponentMethod("calendars","getCal")(r).fromJD(S).formatDate("yyyy-mm-dd")}catch(t){a=v("G%Y-%m-%d")(new Date(w))}if("-"===a.charAt(0))for(;a.length<11;)a="-0"+a.substr(1);else for(;a.length<10;)a="0"+a;o=e=n+f&&t<=i-f))return u;var e=Math.floor(10*l(t+.05,1)),r=new Date(Math.round(t-e/10));return M(a("%Y-%m-%d")(r),r.getHours(),r.getMinutes(),r.getSeconds(),10*r.getUTCMilliseconds()+e)},r.cleanDate=function(t,e,n){if(t===u)return e;if(r.isJSDate(t)||"number"==typeof t&&isFinite(t)){if(_(n))return s.error("JS Dates and milliseconds are incompatible with world calendars",t),e;if(!(t=r.ms2DateTimeLocal(+t))&&void 0!==e)return e}else if(!r.isDateTime(t,n))return s.error("unrecognized date",t),e;return t};var S=/%\d?f/g,E=/%h/g,L={1:"1",2:"1",3:"2",4:"2"};function C(t,e,r,n){t=t.replace(S,(function(t){var r=Math.min(+t.charAt(1)||6,6);return(e/1e3%1+2).toFixed(r).substr(2).replace(/0+$/,"")||"0"}));var i=new Date(Math.floor(e+.05));if(t=t.replace(E,(function(){return L[r("%q")(i)]})),_(n))try{t=g.getComponentMethod("calendars","worldCalFmt")(t,e,n)}catch(t){return"Invalid"}return r(t)(i)}var P=[59,59.9,59.99,59.999,59.9999];r.formatDate=function(t,e,r,n,i,a){if(i=_(i)&&i,!e)if("y"===r)e=a.year;else if("m"===r)e=a.month;else{if("d"!==r)return function(t,e){var r=l(t+.05,f),n=w(Math.floor(r/h),2)+":"+w(l(Math.floor(r/p),60),2);if("M"!==e){o(e)||(e=0);var i=(100+Math.min(l(t/d,60),P[e])).toFixed(e).substr(1);e>0&&(i=i.replace(/0+$/,"").replace(/[\.]$/,"")),n+=":"+i}return n}(t,r)+"\n"+C(a.dayMonthYear,t,n,i);e=a.dayMonth+"\n"+a.year}return C(e,t,n,i)};var I=3*f;r.incrementMonth=function(t,e,r){r=_(r)&&r;var n=l(t,f);if(t=Math.round(t-n),r)try{var i=Math.round(t/f)+m,a=g.getComponentMethod("calendars","getCal")(r),o=a.fromJD(i);return e%12?a.add(o,e,"m"):a.add(o,e/12,"y"),(o.toJD()-m)*f+n}catch(e){s.error("invalid ms "+t+" in calendar "+r)}var c=new Date(t+I);return c.setUTCMonth(c.getUTCMonth()+e)+n-I},r.findExactDates=function(t,e){for(var r,n,i=0,a=0,s=0,l=0,c=_(e)&&g.getComponentMethod("calendars","getCal")(e),u=0;u0&&t[e+1][0]<0)return e;return null}switch(e="RUS"===s||"FJI"===s?function(t){var e;if(null===c(t))e=t;else for(e=new Array(t.length),i=0;ie?r[n++]=[t[i][0]+360,t[i][1]]:i===e?(r[n++]=t[i],r[n++]=[t[i][0],-90]):r[n++]=t[i];var a=h.tester(r);a.pts.pop(),l.push(a)}:function(t){l.push(h.tester(t))},a.type){case"MultiPolygon":for(r=0;ri&&(i=c,e=l)}else e=r;return o.default(e).geometry.coordinates}(u),n.fIn=t,n.fOut=u,s.push(u)}else c.log(["Location",n.loc,"does not have a valid GeoJSON geometry.","Traces with locationmode *geojson-id* only support","*Polygon* and *MultiPolygon* geometries."].join(" "))}delete i[r]}switch(r.type){case"FeatureCollection":var h=r.features;for(n=0;n100?(clearInterval(a),n("Unexpected error while fetching from "+t)):void i++}),50)}))}for(var o=0;o0&&(r.push(i),i=[])}return i.length>0&&r.push(i),r},r.makeLine=function(t){return 1===t.length?{type:"LineString",coordinates:t[0]}:{type:"MultiLineString",coordinates:t}},r.makePolygon=function(t){if(1===t.length)return{type:"Polygon",coordinates:t};for(var e=new Array(t.length),r=0;r1||m<0||m>1?null:{x:t+l*m,y:e+f*m}}function l(t,e,r,n,i){var a=n*t+i*e;if(a<0)return n*n+i*i;if(a>r){var o=n-t,s=i-e;return o*o+s*s}var l=n*e-i*t;return l*l/r}r.segmentsIntersect=s,r.segmentDistance=function(t,e,r,n,i,a,o,c){if(s(t,e,r,n,i,a,o,c))return 0;var u=r-t,f=n-e,h=o-i,p=c-a,d=u*u+f*f,m=h*h+p*p,g=Math.min(l(u,f,d,i-t,a-e),l(u,f,d,o-t,c-e),l(h,p,m,t-i,e-a),l(h,p,m,r-i,n-a));return Math.sqrt(g)},r.getTextLocation=function(t,e,r,s){if(t===i&&s===a||(n={},i=t,a=s),n[r])return n[r];var l=t.getPointAtLength(o(r-s/2,e)),c=t.getPointAtLength(o(r+s/2,e)),u=Math.atan((c.y-l.y)/(c.x-l.x)),f=t.getPointAtLength(o(r,e)),h={x:(4*f.x+l.x+c.x)/6,y:(4*f.y+l.y+c.y)/6,theta:u};return n[r]=h,h},r.clearLocationCache=function(){i=null},r.getVisibleSegment=function(t,e,r){var n,i,a=e.left,o=e.right,s=e.top,l=e.bottom,c=0,u=t.getTotalLength(),f=u;function h(e){var r=t.getPointAtLength(e);0===e?n=r:e===u&&(i=r);var c=r.xo?r.x-o:0,f=r.yl?r.y-l:0;return Math.sqrt(c*c+f*f)}for(var p=h(c);p;){if((c+=p+r)>f)return;p=h(c)}for(p=h(f);p;){if(c>(f-=p+r))return;p=h(f)}return{min:c,max:f,len:f-c,total:u,isClosed:0===c&&f===u&&Math.abs(n.x-i.x)<.1&&Math.abs(n.y-i.y)<.1}},r.findPointOnPath=function(t,e,r,n){for(var i,a,o,s=(n=n||{}).pathLength||t.getTotalLength(),l=n.tolerance||.001,c=n.iterationLimit||30,u=t.getPointAtLength(0)[r]>t.getPointAtLength(s)[r]?-1:1,f=0,h=0,p=s;f0?p=i:h=i,f++}return a}},{"./mod":510}],499:[function(t,e,r){"use strict";var n=t("fast-isnumeric"),i=t("tinycolor2"),a=t("color-normalize"),o=t("../components/colorscale"),s=t("../components/color/attributes").defaultLine,l=t("./array").isArrayOrTypedArray,c=a(s);function u(t,e){var r=t;return r[3]*=e,r}function f(t){if(n(t))return c;var e=a(t);return e.length?e:c}function h(t){return n(t)?t:1}e.exports={formatColor:function(t,e,r){var n,i,s,p,d,m=t.color,g=l(m),v=l(e),y=o.extractOpts(t),x=[];if(n=void 0!==y.colorscale?o.makeColorScaleFuncFromTrace(t):f,i=g?function(t,e){return void 0===t[e]?c:a(n(t[e]))}:f,s=v?function(t,e){return void 0===t[e]?1:h(t[e])}:h,g||v)for(var b=0;b1?(r*t+r*e)/r:t+e,i=String(n).length;if(i>16){var a=String(e).length;if(i>=String(t).length+a){var o=parseFloat(n).toPrecision(12);-1===o.indexOf("e+")&&(n=+o)}}return n}},{}],503:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("d3-time-format").utcFormat,a=t("d3-format").format,o=t("fast-isnumeric"),s=t("../constants/numerical"),l=s.FP_SAFE,c=-l,u=s.BADNUM,f=e.exports={};f.adjustFormat=function(t){return!t||/^\d[.]\df/.test(t)||/[.]\d%/.test(t)?t:"0.f"===t?"~f":/^\d%/.test(t)?"~%":/^\ds/.test(t)?"~s":!/^[~,.0$]/.test(t)&&/[&fps]/.test(t)?"~"+t:t};var h={};f.warnBadFormat=function(t){var e=String(t);h[e]||(h[e]=1,f.warn('encountered bad format: "'+e+'"'))},f.noFormat=function(t){return String(t)},f.numberFormat=function(t){var e;try{e=a(f.adjustFormat(t))}catch(e){return f.warnBadFormat(t),f.noFormat}return e},f.nestedProperty=t("./nested_property"),f.keyedContainer=t("./keyed_container"),f.relativeAttr=t("./relative_attr"),f.isPlainObject=t("./is_plain_object"),f.toLogRange=t("./to_log_range"),f.relinkPrivateKeys=t("./relink_private");var p=t("./array");f.isTypedArray=p.isTypedArray,f.isArrayOrTypedArray=p.isArrayOrTypedArray,f.isArray1D=p.isArray1D,f.ensureArray=p.ensureArray,f.concat=p.concat,f.maxRowLength=p.maxRowLength,f.minRowLength=p.minRowLength;var d=t("./mod");f.mod=d.mod,f.modHalf=d.modHalf;var m=t("./coerce");f.valObjectMeta=m.valObjectMeta,f.coerce=m.coerce,f.coerce2=m.coerce2,f.coerceFont=m.coerceFont,f.coercePattern=m.coercePattern,f.coerceHoverinfo=m.coerceHoverinfo,f.coerceSelectionMarkerOpacity=m.coerceSelectionMarkerOpacity,f.validate=m.validate;var g=t("./dates");f.dateTime2ms=g.dateTime2ms,f.isDateTime=g.isDateTime,f.ms2DateTime=g.ms2DateTime,f.ms2DateTimeLocal=g.ms2DateTimeLocal,f.cleanDate=g.cleanDate,f.isJSDate=g.isJSDate,f.formatDate=g.formatDate,f.incrementMonth=g.incrementMonth,f.dateTick0=g.dateTick0,f.dfltRange=g.dfltRange,f.findExactDates=g.findExactDates,f.MIN_MS=g.MIN_MS,f.MAX_MS=g.MAX_MS;var v=t("./search");f.findBin=v.findBin,f.sorterAsc=v.sorterAsc,f.sorterDes=v.sorterDes,f.distinctVals=v.distinctVals,f.roundUp=v.roundUp,f.sort=v.sort,f.findIndexOfMin=v.findIndexOfMin,f.sortObjectKeys=t("./sort_object_keys");var y=t("./stats");f.aggNums=y.aggNums,f.len=y.len,f.mean=y.mean,f.median=y.median,f.midRange=y.midRange,f.variance=y.variance,f.stdev=y.stdev,f.interp=y.interp;var x=t("./matrix");f.init2dArray=x.init2dArray,f.transposeRagged=x.transposeRagged,f.dot=x.dot,f.translationMatrix=x.translationMatrix,f.rotationMatrix=x.rotationMatrix,f.rotationXYMatrix=x.rotationXYMatrix,f.apply3DTransform=x.apply3DTransform,f.apply2DTransform=x.apply2DTransform,f.apply2DTransform2=x.apply2DTransform2,f.convertCssMatrix=x.convertCssMatrix,f.inverseTransformMatrix=x.inverseTransformMatrix;var b=t("./angles");f.deg2rad=b.deg2rad,f.rad2deg=b.rad2deg,f.angleDelta=b.angleDelta,f.angleDist=b.angleDist,f.isFullCircle=b.isFullCircle,f.isAngleInsideSector=b.isAngleInsideSector,f.isPtInsideSector=b.isPtInsideSector,f.pathArc=b.pathArc,f.pathSector=b.pathSector,f.pathAnnulus=b.pathAnnulus;var _=t("./anchor_utils");f.isLeftAnchor=_.isLeftAnchor,f.isCenterAnchor=_.isCenterAnchor,f.isRightAnchor=_.isRightAnchor,f.isTopAnchor=_.isTopAnchor,f.isMiddleAnchor=_.isMiddleAnchor,f.isBottomAnchor=_.isBottomAnchor;var w=t("./geometry2d");f.segmentsIntersect=w.segmentsIntersect,f.segmentDistance=w.segmentDistance,f.getTextLocation=w.getTextLocation,f.clearLocationCache=w.clearLocationCache,f.getVisibleSegment=w.getVisibleSegment,f.findPointOnPath=w.findPointOnPath;var T=t("./extend");f.extendFlat=T.extendFlat,f.extendDeep=T.extendDeep,f.extendDeepAll=T.extendDeepAll,f.extendDeepNoArrays=T.extendDeepNoArrays;var k=t("./loggers");f.log=k.log,f.warn=k.warn,f.error=k.error;var A=t("./regex");f.counterRegex=A.counter;var M=t("./throttle");f.throttle=M.throttle,f.throttleDone=M.done,f.clearThrottle=M.clear;var S=t("./dom");function E(t){var e={};for(var r in t)for(var n=t[r],i=0;il||t=e)&&(o(t)&&t>=0&&t%1==0)},f.noop=t("./noop"),f.identity=t("./identity"),f.repeat=function(t,e){for(var r=new Array(e),n=0;nr?Math.max(r,Math.min(e,t)):Math.max(e,Math.min(r,t))},f.bBoxIntersect=function(t,e,r){return r=r||0,t.left<=e.right+r&&e.left<=t.right+r&&t.top<=e.bottom+r&&e.top<=t.bottom+r},f.simpleMap=function(t,e,r,n,i){for(var a=t.length,o=new Array(a),s=0;s=Math.pow(2,r)?i>10?(f.warn("randstr failed uniqueness"),l):t(e,r,n,(i||0)+1):l},f.OptionControl=function(t,e){t||(t={}),e||(e="opt");var r={optionList:[],_newoption:function(n){n[e]=t,r[n.name]=n,r.optionList.push(n)}};return r["_"+e]=t,r},f.smooth=function(t,e){if((e=Math.round(e)||0)<2)return t;var r,n,i,a,o=t.length,s=2*o,l=2*e-1,c=new Array(l),u=new Array(o);for(r=0;r=s&&(i-=s*Math.floor(i/s)),i<0?i=-1-i:i>=o&&(i=s-1-i),a+=t[i]*c[n];u[r]=a}return u},f.syncOrAsync=function(t,e,r){var n;function i(){return f.syncOrAsync(t,e,r)}for(;t.length;)if((n=(0,t.splice(0,1)[0])(e))&&n.then)return n.then(i);return r&&r(e)},f.stripTrailingSlash=function(t){return"/"===t.substr(-1)?t.substr(0,t.length-1):t},f.noneOrAll=function(t,e,r){if(t){var n,i=!1,a=!0;for(n=0;n0?e:0}))},f.fillArray=function(t,e,r,n){if(n=n||f.identity,f.isArrayOrTypedArray(t))for(var i=0;i1?i+o[1]:"";if(a&&(o.length>1||s.length>4||r))for(;n.test(s);)s=s.replace(n,"$1"+a+"$2");return s+l},f.TEMPLATE_STRING_REGEX=/%{([^\s%{}:]*)([:|\|][^}]*)?}/g;var z=/^\w*$/;f.templateString=function(t,e){var r={};return t.replace(f.TEMPLATE_STRING_REGEX,(function(t,n){var i;return z.test(n)?i=e[n]:(r[n]=r[n]||f.nestedProperty(e,n).get,i=r[n]()),f.isValidTextValue(i)?i:""}))};var D={max:10,count:0,name:"hovertemplate"};f.hovertemplateString=function(){return B.apply(D,arguments)};var R={max:10,count:0,name:"texttemplate"};f.texttemplateString=function(){return B.apply(R,arguments)};var F=/^[:|\|]/;function B(t,e,r){var n=this,a=arguments;e||(e={});var o={};return t.replace(f.TEMPLATE_STRING_REGEX,(function(t,s,l){var c,u,h,p="_xother"===s||"_yother"===s,d="_xother_"===s||"_yother_"===s,m="xother_"===s||"yother_"===s,g="xother"===s||"yother"===s||p||m||d,v=s;if((p||d)&&(v=v.substring(1)),(m||d)&&(v=v.substring(0,v.length-1)),g){if(void 0===(c=e[v]))return""}else for(h=3;h=48&&o<=57,c=s>=48&&s<=57;if(l&&(n=10*n+o-48),c&&(i=10*i+s-48),!l||!c){if(n!==i)return n-i;if(o!==s)return o-s}}return i-n};var N=2e9;f.seedPseudoRandom=function(){N=2e9},f.pseudoRandom=function(){var t=N;return N=(69069*N+1)%4294967296,Math.abs(N-t)<429496729?f.pseudoRandom():N/4294967296},f.fillText=function(t,e,r){var n=Array.isArray(r)?function(t){r.push(t)}:function(t){r.text=t},i=f.extractOption(t,e,"htx","hovertext");if(f.isValidTextValue(i))return n(i);var a=f.extractOption(t,e,"tx","text");return f.isValidTextValue(a)?n(a):void 0},f.isValidTextValue=function(t){return t||0===t},f.formatPercent=function(t,e){e=e||0;for(var r=(Math.round(100*t*Math.pow(10,e))*Math.pow(.1,e)).toFixed(e)+"%",n=0;n1&&(c=1):c=0,f.strTranslate(i-c*(r+o),a-c*(n+s))+f.strScale(c)+(l?"rotate("+l+(e?"":" "+r+" "+n)+")":"")},f.ensureUniformFontSize=function(t,e){var r=f.extendFlat({},e);return r.size=Math.max(e.size,t._fullLayout.uniformtext.minsize||0),r},f.join2=function(t,e,r){var n=t.length;return n>1?t.slice(0,-1).join(e)+r+t[n-1]:t.join(e)},f.bigFont=function(t){return Math.round(1.2*t)};var j=f.getFirefoxVersion(),U=null!==j&&j<86;f.getPositionFromD3Event=function(){return U?[n.event.layerX,n.event.layerY]:[n.event.offsetX,n.event.offsetY]}},{"../constants/numerical":479,"./anchor_utils":483,"./angles":484,"./array":485,"./clean_number":486,"./clear_responsive":488,"./coerce":489,"./dates":490,"./dom":491,"./extend":493,"./filter_unique":494,"./filter_visible":495,"./geometry2d":498,"./identity":501,"./increment":502,"./is_plain_object":504,"./keyed_container":505,"./localize":506,"./loggers":507,"./make_trace_groups":508,"./matrix":509,"./mod":510,"./nested_property":511,"./noop":512,"./notifier":513,"./preserve_drawing_buffer":517,"./push_unique":518,"./regex":520,"./relative_attr":521,"./relink_private":522,"./search":523,"./sort_object_keys":526,"./stats":527,"./throttle":530,"./to_log_range":531,"@plotly/d3":58,"d3-format":112,"d3-time-format":120,"fast-isnumeric":190}],504:[function(t,e,r){"use strict";e.exports=function(t){return window&&window.process&&window.process.versions?"[object Object]"===Object.prototype.toString.call(t):"[object Object]"===Object.prototype.toString.call(t)&&Object.getPrototypeOf(t).hasOwnProperty("hasOwnProperty")}},{}],505:[function(t,e,r){"use strict";var n=t("./nested_property"),i=/^\w*$/;e.exports=function(t,e,r,a){var o,s,l;r=r||"name",a=a||"value";var c={};e&&e.length?(l=n(t,e),s=l.get()):s=t,e=e||"";var u={};if(s)for(o=0;o2)return c[e]=2|c[e],h.set(t,null);if(f){for(o=e;o1){var e=["LOG:"];for(t=0;t1){var r=[];for(t=0;t"),"long")}},a.warn=function(){var t;if(n.logging>0){var e=["WARN:"];for(t=0;t0){var r=[];for(t=0;t"),"stick")}},a.error=function(){var t;if(n.logging>0){var e=["ERROR:"];for(t=0;t0){var r=[];for(t=0;t"),"stick")}}},{"../plot_api/plot_config":541,"./notifier":513}],508:[function(t,e,r){"use strict";var n=t("@plotly/d3");e.exports=function(t,e,r){var i=t.selectAll("g."+r.replace(/\s/g,".")).data(e,(function(t){return t[0].trace.uid}));i.exit().remove(),i.enter().append("g").attr("class",r),i.order();var a=t.classed("rangeplot")?"nodeRangePlot3":"node3";return i.each((function(t){t[0][a]=n.select(this)})),i}},{"@plotly/d3":58}],509:[function(t,e,r){"use strict";var n=t("gl-mat4");r.init2dArray=function(t,e){for(var r=new Array(t),n=0;ne/2?t-Math.round(t/e)*e:t}}},{}],511:[function(t,e,r){"use strict";var n=t("fast-isnumeric"),i=t("./array").isArrayOrTypedArray;function a(t,e){return function(){var r,n,o,s,l,c=t;for(s=0;s/g),l=0;la||c===i||cs)&&(!e||!l(t))}:function(t,e){var l=t[0],c=t[1];if(l===i||la||c===i||cs)return!1;var u,f,h,p,d,m=r.length,g=r[0][0],v=r[0][1],y=0;for(u=1;uMath.max(f,g)||c>Math.max(h,v)))if(cu||Math.abs(n(o,h))>i)return!0;return!1},a.filter=function(t,e){var r=[t[0]],n=0,i=0;function o(o){t.push(o);var s=r.length,l=n;r.splice(i+1);for(var c=l+1;c1&&o(t.pop());return{addPt:o,raw:t,filtered:r}}},{"../constants/numerical":479,"./matrix":509}],516:[function(t,e,r){(function(r){(function(){"use strict";var n=t("./show_no_webgl_msg"),i=t("regl");e.exports=function(t,e,a){var o=t._fullLayout,s=!0;return o._glcanvas.each((function(n){if(n.regl)n.regl.preloadCachedCode(a);else if(!n.pick||o._has("parcoords")){try{n.regl=i({canvas:this,attributes:{antialias:!n.pick,preserveDrawingBuffer:!0},pixelRatio:t._context.plotGlPixelRatio||r.devicePixelRatio,extensions:e||[],cachedCode:a||{}})}catch(t){s=!1}n.regl||(s=!1),s&&this.addEventListener("webglcontextlost",(function(e){t&&t.emit&&t.emit("plotly_webglcontextlost",{event:e,layer:n.key})}),!1)}})),s||n({container:o._glcontainer.node()}),s}}).call(this)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"./show_no_webgl_msg":525,regl:283}],517:[function(t,e,r){"use strict";var n=t("fast-isnumeric"),i=t("is-mobile");e.exports=function(t){var e;if("string"!=typeof(e=t&&t.hasOwnProperty("userAgent")?t.userAgent:function(){var t;"undefined"!=typeof navigator&&(t=navigator.userAgent);t&&t.headers&&"string"==typeof t.headers["user-agent"]&&(t=t.headers["user-agent"]);return t}()))return!0;var r=i({ua:{headers:{"user-agent":e}},tablet:!0,featureDetect:!1});if(!r)for(var a=e.split(" "),o=1;o-1;s--){var l=a[s];if("Version/"===l.substr(0,8)){var c=l.substr(8).split(".")[0];if(n(c)&&(c=+c),c>=13)return!0}}}return r}},{"fast-isnumeric":190,"is-mobile":234}],518:[function(t,e,r){"use strict";e.exports=function(t,e){if(e instanceof RegExp){for(var r=e.toString(),n=0;ni.queueLength&&(t.undoQueue.queue.shift(),t.undoQueue.index--))},startSequence:function(t){t.undoQueue=t.undoQueue||{index:0,queue:[],sequence:!1},t.undoQueue.sequence=!0,t.undoQueue.beginSequence=!0},stopSequence:function(t){t.undoQueue=t.undoQueue||{index:0,queue:[],sequence:!1},t.undoQueue.sequence=!1,t.undoQueue.beginSequence=!1},undo:function(t){var e,r;if(!(void 0===t.undoQueue||isNaN(t.undoQueue.index)||t.undoQueue.index<=0)){for(t.undoQueue.index--,e=t.undoQueue.queue[t.undoQueue.index],t.undoQueue.inSequence=!0,r=0;r=t.undoQueue.queue.length)){for(e=t.undoQueue.queue[t.undoQueue.index],t.undoQueue.inSequence=!0,r=0;re}function u(t,e){return t>=e}r.findBin=function(t,e,r){if(n(e.start))return r?Math.ceil((t-e.start)/e.size-1e-9)-1:Math.floor((t-e.start)/e.size+1e-9);var a,o,f=0,h=e.length,p=0,d=h>1?(e[h-1]-e[0])/(h-1):1;for(o=d>=0?r?s:l:r?u:c,t+=1e-9*d*(r?-1:1)*(d>=0?1:-1);f90&&i.log("Long binary search..."),f-1},r.sorterAsc=function(t,e){return t-e},r.sorterDes=function(t,e){return e-t},r.distinctVals=function(t){var e,n=t.slice();for(n.sort(r.sorterAsc),e=n.length-1;e>-1&&n[e]===o;e--);for(var i,a=n[e]-n[0]||1,s=a/(e||1)/1e4,l=[],c=0;c<=e;c++){var u=n[c],f=u-i;void 0===i?(l.push(u),i=u):f>s&&(a=Math.min(a,f),l.push(u),i=u)}return{vals:l,minDiff:a}},r.roundUp=function(t,e,r){for(var n,i=0,a=e.length-1,o=0,s=r?0:1,l=r?1:0,c=r?Math.ceil:Math.floor;i0&&(n=1),r&&n)return t.sort(e)}return n?t:t.reverse()},r.findIndexOfMin=function(t,e){e=e||a;for(var r,n=1/0,i=0;ia.length)&&(o=a.length),n(e)||(e=!1),i(a[0])){for(l=new Array(o),s=0;st.length-1)return t[t.length-1];var r=e%1;return r*t[Math.ceil(e)]+(1-r)*t[Math.floor(e)]}},{"./array":485,"fast-isnumeric":190}],528:[function(t,e,r){"use strict";var n=t("color-normalize");e.exports=function(t){return t?n(t):[0,0,0,1]}},{"color-normalize":89}],529:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../lib"),a=i.strTranslate,o=t("../constants/xmlns_namespaces"),s=t("../constants/alignment").LINE_SPACING,l=/([^$]*)([$]+[^$]*[$]+)([^$]*)/;r.convertToTspans=function(t,e,m){var M=t.text(),S=!t.attr("data-notex")&&e&&e._context.typesetMath&&"undefined"!=typeof MathJax&&M.match(l),C=n.select(t.node().parentNode);if(!C.empty()){var P=t.attr("class")?t.attr("class").split(" ")[0]:"text";return P+="-math",C.selectAll("svg."+P).remove(),C.selectAll("g."+P+"-group").remove(),t.style("display",null).attr({"data-unformatted":M,"data-math":"N"}),S?(e&&e._promises||[]).push(new Promise((function(e){t.style("display","none");var r=parseInt(t.node().style.fontSize,10),o={fontSize:r};!function(t,e,r){var a,o,s,l,h=parseInt((MathJax.version||"").split(".")[0]);if(2!==h&&3!==h)return void i.warn("No MathJax version:",MathJax.version);var p=function(){var r="math-output-"+i.randstr({},64),a=(l=n.select("body").append("div").attr({id:r}).style({visibility:"hidden",position:"absolute","font-size":e.fontSize+"px"}).text(t.replace(c,"\\lt ").replace(u,"\\gt "))).node();return 2===h?MathJax.Hub.Typeset(a):MathJax.typeset([a])},d=function(){var e=l.select(2===h?".MathJax_SVG":".MathJax"),a=!e.empty()&&l.select("svg").node();if(a){var o,s=a.getBoundingClientRect();o=2===h?n.select("body").select("#MathJax_SVG_glyphs"):e.select("defs"),r(e,o,s)}else i.log("There was an error in the tex syntax.",t),r();l.remove()};2===h?MathJax.Hub.Queue((function(){return o=i.extendDeepAll({},MathJax.Hub.config),s=MathJax.Hub.processSectionDelay,void 0!==MathJax.Hub.processSectionDelay&&(MathJax.Hub.processSectionDelay=0),MathJax.Hub.Config({messageStyle:"none",tex2jax:{inlineMath:f},displayAlign:"left"})}),(function(){if("SVG"!==(a=MathJax.Hub.config.menuSettings.renderer))return MathJax.Hub.setRenderer("SVG")}),p,d,(function(){if("SVG"!==a)return MathJax.Hub.setRenderer(a)}),(function(){return void 0!==s&&(MathJax.Hub.processSectionDelay=s),MathJax.Hub.Config(o)})):3===h&&(o=i.extendDeepAll({},MathJax.config),MathJax.config.tex||(MathJax.config.tex={}),MathJax.config.tex.inlineMath=f,"svg"!==(a=MathJax.config.startup.output)&&(MathJax.config.startup.output="svg"),MathJax.startup.defaultReady(),MathJax.startup.promise.then((function(){p(),d(),"svg"!==a&&(MathJax.config.startup.output=a),MathJax.config=o})))}(S[2],o,(function(n,i,o){C.selectAll("svg."+P).remove(),C.selectAll("g."+P+"-group").remove();var s=n&&n.select("svg");if(!s||!s.node())return I(),void e();var l=C.append("g").classed(P+"-group",!0).attr({"pointer-events":"none","data-unformatted":M,"data-math":"Y"});l.node().appendChild(s.node()),i&&i.node()&&s.node().insertBefore(i.node().cloneNode(!0),s.node().firstChild);var c=o.width,u=o.height;s.attr({class:P,height:u,preserveAspectRatio:"xMinYMin meet"}).style({overflow:"visible","pointer-events":"none"});var f=t.node().style.fill||"black",h=s.select("g");h.attr({fill:f,stroke:f});var p=h.node().getBoundingClientRect(),d=p.width,g=p.height;(d>c||g>u)&&(s.style("overflow","hidden"),d=(p=s.node().getBoundingClientRect()).width,g=p.height);var v=+t.attr("x"),y=+t.attr("y"),x=-(r||t.node().getBoundingClientRect().height)/4;if("y"===P[0])l.attr({transform:"rotate("+[-90,v,y]+")"+a(-d/2,x-g/2)});else if("l"===P[0])y=x-g/2;else if("a"===P[0]&&0!==P.indexOf("atitle"))v=0,y=x;else{var b=t.attr("text-anchor");v-=d*("middle"===b?.5:"end"===b?1:0),y=y+x-g/2}s.attr({x:v,y:y}),m&&m.call(t,l),e(l)}))}))):I(),t}function I(){C.empty()||(P=t.attr("class")+"-math",C.select("svg."+P).remove()),t.text("").style("white-space","pre"),function(t,e){e=e.replace(g," ");var r,a=!1,l=[],c=-1;function u(){c++;var e=document.createElementNS(o.svg,"tspan");n.select(e).attr({class:"line",dy:c*s+"em"}),t.appendChild(e),r=e;var i=l;if(l=[{node:e}],i.length>1)for(var a=1;a doesnt match end tag <"+t+">. Pretending it did match.",e),r=l[l.length-1].node}else i.log("Ignoring unexpected end tag .",e)}x.test(e)?u():(r=t,l=[{node:t}]);for(var S=e.split(v),C=0;C|>|>)/g;var f=[["$","$"],["\\(","\\)"]];var h={sup:"font-size:70%",sub:"font-size:70%",b:"font-weight:bold",i:"font-style:italic",a:"cursor:pointer",span:"",em:"font-style:italic;font-weight:bold"},p={sub:"0.3em",sup:"-0.6em"},d={sub:"-0.21em",sup:"0.42em"},m=["http:","https:","mailto:","",void 0,":"],g=r.NEWLINES=/(\r\n?|\n)/g,v=/(<[^<>]*>)/,y=/<(\/?)([^ >]*)(\s+(.*))?>/i,x=//i;r.BR_TAG_ALL=//gi;var b=/(^|[\s"'])style\s*=\s*("([^"]*);?"|'([^']*);?')/i,_=/(^|[\s"'])href\s*=\s*("([^"]*)"|'([^']*)')/i,w=/(^|[\s"'])target\s*=\s*("([^"\s]*)"|'([^'\s]*)')/i,T=/(^|[\s"'])popup\s*=\s*("([\w=,]*)"|'([\w=,]*)')/i;function k(t,e){if(!t)return null;var r=t.match(e),n=r&&(r[3]||r[4]);return n&&E(n)}var A=/(^|;)\s*color:/;r.plainText=function(t,e){for(var r=void 0!==(e=e||{}).len&&-1!==e.len?e.len:1/0,n=void 0!==e.allowedTags?e.allowedTags:["br"],i="...".length,a=t.split(v),o=[],s="",l=0,c=0;ci?o.push(u.substr(0,d-i)+"..."):o.push(u.substr(0,d));break}s=""}}return o.join("")};var M={mu:"\u03bc",amp:"&",lt:"<",gt:">",nbsp:"\xa0",times:"\xd7",plusmn:"\xb1",deg:"\xb0"},S=/&(#\d+|#x[\da-fA-F]+|[a-z]+);/g;function E(t){return t.replace(S,(function(t,e){return("#"===e.charAt(0)?function(t){if(t>1114111)return;var e=String.fromCodePoint;if(e)return e(t);var r=String.fromCharCode;return t<=65535?r(t):r(55232+(t>>10),t%1024+56320)}("x"===e.charAt(1)?parseInt(e.substr(2),16):parseInt(e.substr(1),10)):M[e])||t}))}function L(t){var e=encodeURI(decodeURI(t)),r=document.createElement("a"),n=document.createElement("a");r.href=t,n.href=e;var i=r.protocol,a=n.protocol;return-1!==m.indexOf(i)&&-1!==m.indexOf(a)?e:""}function C(t,e,r){var n,a,o,s=r.horizontalAlign,l=r.verticalAlign||"top",c=t.node().getBoundingClientRect(),u=e.node().getBoundingClientRect();return a="bottom"===l?function(){return c.bottom-n.height}:"middle"===l?function(){return c.top+(c.height-n.height)/2}:function(){return c.top},o="right"===s?function(){return c.right-n.width}:"center"===s?function(){return c.left+(c.width-n.width)/2}:function(){return c.left},function(){n=this.node().getBoundingClientRect();var t=o()-u.left,e=a()-u.top,s=r.gd||{};if(r.gd){s._fullLayout._calcInverseTransform(s);var l=i.apply3DTransform(s._fullLayout._invTransform)(t,e);t=l[0],e=l[1]}return this.style({top:e+"px",left:t+"px","z-index":1e3}),this}}r.convertEntities=E,r.sanitizeHTML=function(t){t=t.replace(g," ");for(var e=document.createElement("p"),r=e,i=[],a=t.split(v),o=0;oa.ts+e?l():a.timer=setTimeout((function(){l(),a.timer=null}),e)},r.done=function(t){var e=n[t];return e&&e.timer?new Promise((function(t){var r=e.onDone;e.onDone=function(){r&&r(),t(),e.onDone=null}})):Promise.resolve()},r.clear=function(t){if(t)i(n[t]),delete n[t];else for(var e in n)r.clear(e)}},{}],531:[function(t,e,r){"use strict";var n=t("fast-isnumeric");e.exports=function(t,e){if(t>0)return Math.log(t)/Math.LN10;var r=Math.log(Math.min(e[0],e[1]))/Math.LN10;return n(r)||(r=Math.log(Math.max(e[0],e[1]))/Math.LN10-6),r}},{"fast-isnumeric":190}],532:[function(t,e,r){"use strict";var n=e.exports={},i=t("../plots/geo/constants").locationmodeToLayer,a=t("topojson-client").feature;n.getTopojsonName=function(t){return[t.scope.replace(/ /g,"-"),"_",t.resolution.toString(),"m"].join("")},n.getTopojsonPath=function(t,e){return t+e+".json"},n.getTopojsonFeatures=function(t,e){var r=i[t.locationmode],n=e.objects[r];return a(e,n).features}},{"../plots/geo/constants":587,"topojson-client":315}],533:[function(t,e,r){"use strict";e.exports={moduleType:"locale",name:"en-US",dictionary:{"Click to enter Colorscale title":"Click to enter Colorscale title"},format:{date:"%m/%d/%Y"}}},{}],534:[function(t,e,r){"use strict";e.exports={moduleType:"locale",name:"en",dictionary:{"Click to enter Colorscale title":"Click to enter Colourscale title"},format:{days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],periods:["AM","PM"],dateTime:"%a %b %e %X %Y",date:"%d/%m/%Y",time:"%H:%M:%S",decimal:".",thousands:",",grouping:[3],currency:["$",""],year:"%Y",month:"%b %Y",dayMonth:"%b %-d",dayMonthYear:"%b %-d, %Y"}}},{}],535:[function(t,e,r){"use strict";var n=t("../registry");e.exports=function(t){for(var e,r,i=n.layoutArrayContainers,a=n.layoutArrayRegexes,o=t.split("[")[0],s=0;s0&&o.log("Clearing previous rejected promises from queue."),t._promises=[]},r.cleanLayout=function(t){var e,n;t||(t={}),t.xaxis1&&(t.xaxis||(t.xaxis=t.xaxis1),delete t.xaxis1),t.yaxis1&&(t.yaxis||(t.yaxis=t.yaxis1),delete t.yaxis1),t.scene1&&(t.scene||(t.scene=t.scene1),delete t.scene1);var a=(s.subplotsRegistry.cartesian||{}).attrRegex,l=(s.subplotsRegistry.polar||{}).attrRegex,f=(s.subplotsRegistry.ternary||{}).attrRegex,h=(s.subplotsRegistry.gl3d||{}).attrRegex,m=Object.keys(t);for(e=0;e3?(O.x=1.02,O.xanchor="left"):O.x<-2&&(O.x=-.02,O.xanchor="right"),O.y>3?(O.y=1.02,O.yanchor="bottom"):O.y<-2&&(O.y=-.02,O.yanchor="top")),d(t),"rotate"===t.dragmode&&(t.dragmode="orbit"),c.clean(t),t.template&&t.template.layout&&r.cleanLayout(t.template.layout),t},r.cleanData=function(t){for(var e=0;e0)return t.substr(0,e)}r.hasParent=function(t,e){for(var r=b(e);r;){if(r in t)return!0;r=b(r)}return!1};var _=["x","y","z"];r.clearAxisTypes=function(t,e,r){for(var n=0;n1&&a.warn("Full array edits are incompatible with other edits",f);var y=r[""][""];if(c(y))e.set(null);else{if(!Array.isArray(y))return a.warn("Unrecognized full array edit value",f,y),!0;e.set(y)}return!m&&(h(g,v),p(t),!0)}var x,b,_,w,T,k,A,M,S=Object.keys(r).map(Number).sort(o),E=e.get(),L=E||[],C=u(v,f).get(),P=[],I=-1,O=L.length;for(x=0;xL.length-(A?0:1))a.warn("index out of range",f,_);else if(void 0!==k)T.length>1&&a.warn("Insertion & removal are incompatible with edits to the same index.",f,_),c(k)?P.push(_):A?("add"===k&&(k={}),L.splice(_,0,k),C&&C.splice(_,0,{})):a.warn("Unrecognized full object edit value",f,_,k),-1===I&&(I=_);else for(b=0;b=0;x--)L.splice(P[x],1),C&&C.splice(P[x],1);if(L.length?E||e.set(L):e.set(null),m)return!1;if(h(g,v),d!==i){var z;if(-1===I)z=S;else{for(O=Math.max(L.length,O),z=[],x=0;x=I);x++)z.push(_);for(x=I;x=t.data.length||i<-t.data.length)throw new Error(r+" must be valid indices for gd.data.");if(e.indexOf(i,n+1)>-1||i>=0&&e.indexOf(-t.data.length+i)>-1||i<0&&e.indexOf(t.data.length+i)>-1)throw new Error("each index in "+r+" must be unique.")}}function I(t,e,r){if(!Array.isArray(t.data))throw new Error("gd.data must be an array.");if(void 0===e)throw new Error("currentIndices is a required argument.");if(Array.isArray(e)||(e=[e]),P(t,e,"currentIndices"),void 0===r||Array.isArray(r)||(r=[r]),void 0!==r&&P(t,r,"newIndices"),void 0!==r&&e.length!==r.length)throw new Error("current and new indices must be of equal length.")}function O(t,e,r,n,a){!function(t,e,r,n){var i=o.isPlainObject(n);if(!Array.isArray(t.data))throw new Error("gd.data must be an array");if(!o.isPlainObject(e))throw new Error("update must be a key:value object");if(void 0===r)throw new Error("indices must be an integer or array of integers");for(var a in P(t,r,"indices"),e){if(!Array.isArray(e[a])||e[a].length!==r.length)throw new Error("attribute "+a+" must be an array of length equal to indices array length");if(i&&(!(a in n)||!Array.isArray(n[a])||n[a].length!==e[a].length))throw new Error("when maxPoints is set as a key:value object it must contain a 1:1 corrispondence with the keys and number of traces in the update object")}}(t,e,r,n);for(var l=function(t,e,r,n){var a,l,c,u,f,h=o.isPlainObject(n),p=[];for(var d in Array.isArray(r)||(r=[r]),r=C(r,t.data.length-1),e)for(var m=0;m-1&&-1===r.indexOf("grouptitlefont")?l(r,r.replace("titlefont","title.font")):r.indexOf("titleposition")>-1?l(r,r.replace("titleposition","title.position")):r.indexOf("titleside")>-1?l(r,r.replace("titleside","title.side")):r.indexOf("titleoffset")>-1&&l(r,r.replace("titleoffset","title.offset")):l(r,r.replace("title","title.text"));function l(e,r){t[r]=t[e],delete t[e]}}function U(t,e,r){t=o.getGraphDiv(t),_.clearPromiseQueue(t);var n={};if("string"==typeof e)n[e]=r;else{if(!o.isPlainObject(e))return o.warn("Relayout fail.",e,r),Promise.reject();n=o.extendFlat({},e)}Object.keys(n).length&&(t.changed=!0);var i=W(t,n),a=i.flags;a.calc&&(t.calcdata=void 0);var s=[h.previousPromises];a.layoutReplot?s.push(w.layoutReplot):Object.keys(n).length&&(V(t,a,i)||h.supplyDefaults(t),a.legend&&s.push(w.doLegend),a.layoutstyle&&s.push(w.layoutStyles),a.axrange&&H(s,i.rangesAltered),a.ticks&&s.push(w.doTicksRelayout),a.modebar&&s.push(w.doModeBar),a.camera&&s.push(w.doCamera),a.colorbars&&s.push(w.doColorBars),s.push(M)),s.push(h.rehover,h.redrag),c.add(t,U,[t,i.undoit],U,[t,i.redoit]);var l=o.syncOrAsync(s,t);return l&&l.then||(l=Promise.resolve(t)),l.then((function(){return t.emit("plotly_relayout",i.eventData),t}))}function V(t,e,r){var n=t._fullLayout;if(!e.axrange)return!1;for(var i in e)if("axrange"!==i&&e[i])return!1;for(var a in r.rangesAltered){var o=p.id2name(a),s=t.layout[o],l=n[o];if(l.autorange=s.autorange,s.range&&(l.range=s.range.slice()),l.cleanRange(),l._matchGroup)for(var c in l._matchGroup)if(c!==a){var u=n[p.id2name(c)];u.autorange=l.autorange,u.range=l.range.slice(),u._input.range=l.range.slice()}}return!0}function H(t,e){var r=e?function(t){var r=[],n=!0;for(var i in e){var a=p.getFromId(t,i);if(r.push(i),-1!==(a.ticklabelposition||"").indexOf("inside")&&a._anchorAxis&&r.push(a._anchorAxis._id),a._matchGroup)for(var o in a._matchGroup)e[o]||r.push(o);a.automargin&&(n=!1)}return p.draw(t,r,{skipTitle:n})}:function(t){return p.draw(t,"redraw")};t.push(y,w.doAutoRangeAndConstraints,r,w.drawData,w.finalDraw)}var q=/^[xyz]axis[0-9]*\.range(\[[0|1]\])?$/,G=/^[xyz]axis[0-9]*\.autorange$/,Y=/^[xyz]axis[0-9]*\.domain(\[[0|1]\])?$/;function W(t,e){var r,n,i,a=t.layout,l=t._fullLayout,c=l._guiEditing,h=F(l._preGUI,c),d=Object.keys(e),m=p.list(t),g=o.extendDeepAll({},e),v={};for(j(e),d=Object.keys(e),n=0;n0&&"string"!=typeof O.parts[D];)D--;var B=O.parts[D],N=O.parts[D-1]+"."+B,U=O.parts.slice(0,D).join("."),V=s(t.layout,U).get(),H=s(l,U).get(),W=O.get();if(void 0!==z){M[I]=z,S[I]="reverse"===B?z:R(W);var Z=f.getLayoutValObject(l,O.parts);if(Z&&Z.impliedEdits&&null!==z)for(var J in Z.impliedEdits)E(o.relativeAttr(I,J),Z.impliedEdits[J]);if(-1!==["width","height"].indexOf(I))if(z){E("autosize",null);var K="height"===I?"width":"height";E(K,l[K])}else l[I]=t._initialAutoSize[I];else if("autosize"===I)E("width",z?null:l.width),E("height",z?null:l.height);else if(N.match(q))P(N),s(l,U+"._inputRange").set(null);else if(N.match(G)){P(N),s(l,U+"._inputRange").set(null);var Q=s(l,U).get();Q._inputDomain&&(Q._input.domain=Q._inputDomain.slice())}else N.match(Y)&&s(l,U+"._inputDomain").set(null);if("type"===B){L=V;var $="linear"===H.type&&"log"===z,tt="log"===H.type&&"linear"===z;if($||tt){if(L&&L.range)if(H.autorange)$&&(L.range=L.range[1]>L.range[0]?[1,2]:[2,1]);else{var et=L.range[0],rt=L.range[1];$?(et<=0&&rt<=0&&E(U+".autorange",!0),et<=0?et=rt/1e6:rt<=0&&(rt=et/1e6),E(U+".range[0]",Math.log(et)/Math.LN10),E(U+".range[1]",Math.log(rt)/Math.LN10)):(E(U+".range[0]",Math.pow(10,et)),E(U+".range[1]",Math.pow(10,rt)))}else E(U+".autorange",!0);Array.isArray(l._subplots.polar)&&l._subplots.polar.length&&l[O.parts[0]]&&"radialaxis"===O.parts[1]&&delete l[O.parts[0]]._subplot.viewInitial["radialaxis.range"],u.getComponentMethod("annotations","convertCoords")(t,H,z,E),u.getComponentMethod("images","convertCoords")(t,H,z,E)}else E(U+".autorange",!0),E(U+".range",null);s(l,U+"._inputRange").set(null)}else if(B.match(k)){var nt=s(l,I).get(),it=(z||{}).type;it&&"-"!==it||(it="linear"),u.getComponentMethod("annotations","convertCoords")(t,nt,it,E),u.getComponentMethod("images","convertCoords")(t,nt,it,E)}var at=b.containerArrayMatch(I);if(at){r=at.array,n=at.index;var ot=at.property,st=Z||{editType:"calc"};""!==n&&""===ot&&(b.isAddVal(z)?S[I]=null:b.isRemoveVal(z)?S[I]=(s(a,r).get()||[])[n]:o.warn("unrecognized full object value",e)),T.update(A,st),v[r]||(v[r]={});var lt=v[r][n];lt||(lt=v[r][n]={}),lt[ot]=z,delete e[I]}else"reverse"===B?(V.range?V.range.reverse():(E(U+".autorange",!0),V.range=[1,0]),H.autorange?A.calc=!0:A.plot=!0):("dragmode"===I&&(!1===z&&!1!==W||!1!==z&&!1===W)||l._has("scatter-like")&&l._has("regl")&&"dragmode"===I&&("lasso"===z||"select"===z)&&"lasso"!==W&&"select"!==W||l._has("gl2d")?A.plot=!0:Z?T.update(A,Z):A.calc=!0,O.set(z))}}for(r in v){b.applyContainerArrayChanges(t,h(a,r),v[r],A,h)||(A.plot=!0)}for(var ct in C){var ut=(L=p.getFromId(t,ct))&&L._constraintGroup;if(ut)for(var ft in A.calc=!0,ut)C[ft]||(p.getFromId(t,ft)._constraintShrinkable=!0)}return(X(t)||e.height||e.width)&&(A.plot=!0),(A.plot||A.calc)&&(A.layoutReplot=!0),{flags:A,rangesAltered:C,undoit:S,redoit:M,eventData:g}}function X(t){var e=t._fullLayout,r=e.width,n=e.height;return t.layout.autosize&&h.plotAutoSize(t,t.layout,e),e.width!==r||e.height!==n}function Z(t,e,n,i){t=o.getGraphDiv(t),_.clearPromiseQueue(t),o.isPlainObject(e)||(e={}),o.isPlainObject(n)||(n={}),Object.keys(e).length&&(t.changed=!0),Object.keys(n).length&&(t.changed=!0);var a=_.coerceTraceIndices(t,i),s=N(t,o.extendFlat({},e),a),l=s.flags,u=W(t,o.extendFlat({},n)),f=u.flags;(l.calc||f.calc)&&(t.calcdata=void 0),l.clearAxisTypes&&_.clearAxisTypes(t,a,n);var p=[];f.layoutReplot?p.push(w.layoutReplot):l.fullReplot?p.push(r._doPlot):(p.push(h.previousPromises),V(t,f,u)||h.supplyDefaults(t),l.style&&p.push(w.doTraceStyle),(l.colorbars||f.colorbars)&&p.push(w.doColorBars),f.legend&&p.push(w.doLegend),f.layoutstyle&&p.push(w.layoutStyles),f.axrange&&H(p,u.rangesAltered),f.ticks&&p.push(w.doTicksRelayout),f.modebar&&p.push(w.doModeBar),f.camera&&p.push(w.doCamera),p.push(M)),p.push(h.rehover,h.redrag),c.add(t,Z,[t,s.undoit,u.undoit,s.traces],Z,[t,s.redoit,u.redoit,s.traces]);var d=o.syncOrAsync(p,t);return d&&d.then||(d=Promise.resolve(t)),d.then((function(){return t.emit("plotly_update",{data:s.eventData,layout:u.eventData}),t}))}function J(t){return function(e){e._fullLayout._guiEditing=!0;var r=t.apply(null,arguments);return e._fullLayout._guiEditing=!1,r}}var K=[{pattern:/^hiddenlabels/,attr:"legend.uirevision"},{pattern:/^((x|y)axis\d*)\.((auto)?range|title\.text)/},{pattern:/axis\d*\.showspikes$/,attr:"modebar.uirevision"},{pattern:/(hover|drag)mode$/,attr:"modebar.uirevision"},{pattern:/^(scene\d*)\.camera/},{pattern:/^(geo\d*)\.(projection|center|fitbounds)/},{pattern:/^(ternary\d*\.[abc]axis)\.(min|title\.text)$/},{pattern:/^(polar\d*\.radialaxis)\.((auto)?range|angle|title\.text)/},{pattern:/^(polar\d*\.angularaxis)\.rotation/},{pattern:/^(mapbox\d*)\.(center|zoom|bearing|pitch)/},{pattern:/^legend\.(x|y)$/,attr:"editrevision"},{pattern:/^(shapes|annotations)/,attr:"editrevision"},{pattern:/^title\.text$/,attr:"editrevision"}],Q=[{pattern:/^selectedpoints$/,attr:"selectionrevision"},{pattern:/(^|value\.)visible$/,attr:"legend.uirevision"},{pattern:/^dimensions\[\d+\]\.constraintrange/},{pattern:/^node\.(x|y|groups)/},{pattern:/^level$/},{pattern:/(^|value\.)name$/},{pattern:/colorbar\.title\.text$/},{pattern:/colorbar\.(x|y)$/,attr:"editrevision"}];function $(t,e){for(var r=0;r1;)if(n.pop(),void 0!==(r=s(e,n.join(".")+".uirevision").get()))return r;return e.uirevision}function et(t,e){for(var r=0;r=i.length?i[0]:i[t]:i}function l(t){return Array.isArray(a)?t>=a.length?a[0]:a[t]:a}function c(t,e){var r=0;return function(){if(t&&++r===e)return t()}}return void 0===n._frameWaitingCnt&&(n._frameWaitingCnt=0),new Promise((function(a,u){function f(){n._currentFrame&&n._currentFrame.onComplete&&n._currentFrame.onComplete();var e=n._currentFrame=n._frameQueue.shift();if(e){var r=e.name?e.name.toString():null;t._fullLayout._currentFrame=r,n._lastFrameAt=Date.now(),n._timeToNext=e.frameOpts.duration,h.transition(t,e.frame.data,e.frame.layout,_.coerceTraceIndices(t,e.frame.traces),e.frameOpts,e.transitionOpts).then((function(){e.onComplete&&e.onComplete()})),t.emit("plotly_animatingframe",{name:r,frame:e.frame,animation:{frame:e.frameOpts,transition:e.transitionOpts}})}else t.emit("plotly_animated"),window.cancelAnimationFrame(n._animationRaf),n._animationRaf=null}function p(){t.emit("plotly_animating"),n._lastFrameAt=-1/0,n._timeToNext=0,n._runningTransitions=0,n._currentFrame=null;var e=function(){n._animationRaf=window.requestAnimationFrame(e),Date.now()-n._lastFrameAt>n._timeToNext&&f()};e()}var d,m,g=0;function v(t){return Array.isArray(i)?g>=i.length?t.transitionOpts=i[g]:t.transitionOpts=i[0]:t.transitionOpts=i,g++,t}var y=[],x=null==e,b=Array.isArray(e);if(!x&&!b&&o.isPlainObject(e))y.push({type:"object",data:v(o.extendFlat({},e))});else if(x||-1!==["string","number"].indexOf(typeof e))for(d=0;d0&&kk)&&A.push(m);y=A}}y.length>0?function(e){if(0!==e.length){for(var i=0;i=0;n--)if(o.isPlainObject(e[n])){var m=e[n].name,g=(u[m]||d[m]||{}).name,v=e[n].name,y=u[g]||d[g];g&&v&&"number"==typeof v&&y&&A<5&&(A++,o.warn('addFrames: overwriting frame "'+(u[g]||d[g]).name+'" with a frame whose name of type "number" also equates to "'+g+'". This is valid but may potentially lead to unexpected behavior since all plotly.js frame names are stored internally as strings.'),5===A&&o.warn("addFrames: This API call has yielded too many of these warnings. For the rest of this call, further warnings about numeric frame names will be suppressed.")),d[m]={name:m},p.push({frame:h.supplyFrameDefaults(e[n]),index:r&&void 0!==r[n]&&null!==r[n]?r[n]:f+n})}p.sort((function(t,e){return t.index>e.index?-1:t.index=0;n--){if("number"==typeof(i=p[n].frame).name&&o.warn("Warning: addFrames accepts frames with numeric names, but the numbers areimplicitly cast to strings"),!i.name)for(;u[i.name="frame "+t._transitionData._counter++];);if(u[i.name]){for(a=0;a=0;r--)n=e[r],a.push({type:"delete",index:n}),s.unshift({type:"insert",index:n,value:i[n]});var l=h.modifyFrames,u=h.modifyFrames,f=[t,s],p=[t,a];return c&&c.add(t,l,f,u,p),h.modifyFrames(t,a)},r.addTraces=function t(e,n,i){e=o.getGraphDiv(e);var a,s,l=[],u=r.deleteTraces,f=t,h=[e,l],p=[e,n];for(function(t,e,r){var n,i;if(!Array.isArray(t.data))throw new Error("gd.data must be an array.");if(void 0===e)throw new Error("traces must be defined.");for(Array.isArray(e)||(e=[e]),n=0;n=0&&r=0&&r=a.length)return!1;if(2===t.dimensions){if(r++,e.length===r)return t;var o=e[r];if(!y(o))return!1;t=a[i][o]}else t=a[i]}else t=a}}return t}function y(t){return t===Math.round(t)&&t>=0}function x(){var t,e,r={};for(t in f(r,o),n.subplotsRegistry){if((e=n.subplotsRegistry[t]).layoutAttributes)if(Array.isArray(e.attr))for(var i=0;i=l.length)return!1;i=(r=(n.transformsRegistry[l[c].type]||{}).attributes)&&r[e[2]],s=3}else{var u=t._module;if(u||(u=(n.modules[t.type||a.type.dflt]||{})._module),!u)return!1;if(!(i=(r=u.attributes)&&r[o])){var f=u.basePlotModule;f&&f.attributes&&(i=f.attributes[o])}i||(i=a[o])}return v(i,e,s)},r.getLayoutValObject=function(t,e){return v(function(t,e){var r,i,a,s,l=t._basePlotModules;if(l){var c;for(r=0;r=i&&(r._input||{})._templateitemname;o&&(a=i);var s,l=e+"["+a+"]";function c(){s={},o&&(s[l]={},s[l].templateitemname=o)}function u(t,e){o?n.nestedProperty(s[l],t).set(e):s[l+"."+t]=e}function f(){var t=s;return c(),t}return c(),{modifyBase:function(t,e){s[t]=e},modifyItem:u,getUpdateObj:f,applyUpdate:function(e,r){e&&u(e,r);var i=f();for(var a in i)n.nestedProperty(t,a).set(i[a])}}}},{"../lib":503,"../plots/attributes":550}],544:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../registry"),a=t("../plots/plots"),o=t("../lib"),s=t("../lib/clear_gl_canvases"),l=t("../components/color"),c=t("../components/drawing"),u=t("../components/titles"),f=t("../components/modebar"),h=t("../plots/cartesian/axes"),p=t("../constants/alignment"),d=t("../plots/cartesian/constraints"),m=d.enforce,g=d.clean,v=t("../plots/cartesian/autorange").doAutoRange;function y(t,e,r){for(var n=0;n=t[1]||i[1]<=t[0])&&(a[0]e[0]))return!0}return!1}function x(t){var e,i,s,u,d,m,g=t._fullLayout,v=g._size,x=v.p,_=h.list(t,"",!0);if(g._paperdiv.style({width:t._context.responsive&&g.autosize&&!t._context._hasZeroWidth&&!t.layout.width?"100%":g.width+"px",height:t._context.responsive&&g.autosize&&!t._context._hasZeroHeight&&!t.layout.height?"100%":g.height+"px"}).selectAll(".main-svg").call(c.setSize,g.width,g.height),t._context.setBackground(t,g.paper_bgcolor),r.drawMainTitle(t),f.manage(t),!g._has("cartesian"))return a.previousPromises(t);function T(t,e,r){var n=t._lw/2;return"x"===t._id.charAt(0)?e?"top"===r?e._offset-x-n:e._offset+e._length+x+n:v.t+v.h*(1-(t.position||0))+n%1:e?"right"===r?e._offset+e._length+x+n:e._offset-x-n:v.l+v.w*(t.position||0)+n%1}for(e=0;e<_.length;e++){var k=(u=_[e])._anchorAxis;u._linepositions={},u._lw=c.crispRound(t,u.linewidth,1),u._mainLinePosition=T(u,k,u.side),u._mainMirrorPosition=u.mirror&&k?T(u,k,p.OPPOSITE_SIDE[u.side]):null}var A=[],M=[],S=[],E=1===l.opacity(g.paper_bgcolor)&&1===l.opacity(g.plot_bgcolor)&&g.paper_bgcolor===g.plot_bgcolor;for(i in g._plots)if((s=g._plots[i]).mainplot)s.bg&&s.bg.remove(),s.bg=void 0;else{var L=s.xaxis.domain,C=s.yaxis.domain,P=s.plotgroup;if(y(L,C,S)){var I=P.node(),O=s.bg=o.ensureSingle(P,"rect","bg");I.insertBefore(O.node(),I.childNodes[0]),M.push(i)}else P.select("rect.bg").remove(),S.push([L,C]),E||(A.push(i),M.push(i))}var z,D,R,F,B,N,j,U,V,H,q,G,Y,W=g._bgLayer.selectAll(".bg").data(A);for(W.enter().append("rect").classed("bg",!0),W.exit().remove(),W.each((function(t){g._plots[t].bg=n.select(this)})),e=0;eT?u.push({code:"unused",traceType:y,templateCount:w,dataCount:T}):T>w&&u.push({code:"reused",traceType:y,templateCount:w,dataCount:T})}}else u.push({code:"data"});if(function t(e,r){for(var n in e)if("_"!==n.charAt(0)){var a=e[n],o=m(e,n,r);i(a)?(Array.isArray(e)&&!1===a._template&&a.templateitemname&&u.push({code:"missing",path:o,templateitemname:a.templateitemname}),t(a,o)):Array.isArray(a)&&g(a)&&t(a,o)}}({data:p,layout:h},""),u.length)return u.map(v)}},{"../lib":503,"../plots/attributes":550,"../plots/plots":619,"./plot_config":541,"./plot_schema":542,"./plot_template":543}],546:[function(t,e,r){"use strict";var n=t("fast-isnumeric"),i=t("./plot_api"),a=t("../plots/plots"),o=t("../lib"),s=t("../snapshot/helpers"),l=t("../snapshot/tosvg"),c=t("../snapshot/svgtoimg"),u=t("../version").version,f={format:{valType:"enumerated",values:["png","jpeg","webp","svg","full-json"],dflt:"png"},width:{valType:"number",min:1},height:{valType:"number",min:1},scale:{valType:"number",min:0,dflt:1},setBackground:{valType:"any",dflt:!1},imageDataOnly:{valType:"boolean",dflt:!1}};e.exports=function(t,e){var r,h,p,d;function m(t){return!(t in e)||o.validate(e[t],f[t])}if(e=e||{},o.isPlainObject(t)?(r=t.data||[],h=t.layout||{},p=t.config||{},d={}):(t=o.getGraphDiv(t),r=o.extendDeep([],t.data),h=o.extendDeep({},t.layout),p=t._context,d=t._fullLayout||{}),!m("width")&&null!==e.width||!m("height")&&null!==e.height)throw new Error("Height and width should be pixel values.");if(!m("format"))throw new Error("Export format is not "+o.join2(f.format.values,", "," or ")+".");var g={};function v(t,r){return o.coerce(e,g,f,t,r)}var y=v("format"),x=v("width"),b=v("height"),_=v("scale"),w=v("setBackground"),T=v("imageDataOnly"),k=document.createElement("div");k.style.position="absolute",k.style.left="-5000px",document.body.appendChild(k);var A=o.extendFlat({},h);x?A.width=x:null===e.width&&n(d.width)&&(A.width=d.width),b?A.height=b:null===e.height&&n(d.height)&&(A.height=d.height);var M=o.extendFlat({},p,{_exportedPlot:!0,staticPlot:!0,setBackground:w}),S=s.getRedrawFunc(k);function E(){return new Promise((function(t){setTimeout(t,s.getDelay(k._fullLayout))}))}function L(){return new Promise((function(t,e){var r=l(k,y,_),n=k._fullLayout.width,f=k._fullLayout.height;function h(){i.purge(k),document.body.removeChild(k)}if("full-json"===y){var p=a.graphJson(k,!1,"keepdata","object",!0,!0);return p.version=u,p=JSON.stringify(p),h(),t(T?p:s.encodeJSON(p))}if(h(),"svg"===y)return t(T?r:s.encodeSVG(r));var d=document.createElement("canvas");d.id=o.randstr(),c({format:y,width:n,height:f,scale:_,canvas:d,svg:r,promise:!0}).then(t).catch(e)}))}return new Promise((function(t,e){i.newPlot(k,r,A,M).then(S).then(E).then(L).then((function(e){t(function(t){return T?t.replace(s.IMAGE_URL_PREFIX,""):t}(e))})).catch((function(t){e(t)}))}))}},{"../lib":503,"../plots/plots":619,"../snapshot/helpers":642,"../snapshot/svgtoimg":644,"../snapshot/tosvg":646,"../version":1123,"./plot_api":540,"fast-isnumeric":190}],547:[function(t,e,r){"use strict";var n=t("../lib"),i=t("../plots/plots"),a=t("./plot_schema"),o=t("./plot_config").dfltConfig,s=n.isPlainObject,l=Array.isArray,c=n.isArrayOrTypedArray;function u(t,e,r,i,a,o){o=o||[];for(var f=Object.keys(t),h=0;hx.length&&i.push(d("unused",a,v.concat(x.length)));var A,M,S,E,L,C=x.length,P=Array.isArray(k);if(P&&(C=Math.min(C,k.length)),2===b.dimensions)for(M=0;Mx[M].length&&i.push(d("unused",a,v.concat(M,x[M].length)));var I=x[M].length;for(A=0;A<(P?Math.min(I,k[M].length):I);A++)S=P?k[M][A]:k,E=y[M][A],L=x[M][A],n.validate(E,S)?L!==E&&L!==+E&&i.push(d("dynamic",a,v.concat(M,A),E,L)):i.push(d("value",a,v.concat(M,A),E))}else i.push(d("array",a,v.concat(M),y[M]));else for(M=0;M1&&p.push(d("object","layout"))),i.supplyDefaults(m);for(var g=m._fullData,v=r.length,y=0;y0&&Math.round(f)===f))return{vals:i};c=f}for(var h=e.calendar,p="start"===l,d="end"===l,m=t[r+"period0"],g=a(m,h)||0,v=[],y=[],x=[],b=i.length,_=0;_A;)k=o(k,-c,h);for(;k<=A;)k=o(k,c,h);T=o(k,-c,h)}else{for(k=g+(w=Math.round((A-g)/u))*u;k>A;)k-=u;for(;k<=A;)k+=u;T=k-u}v[_]=p?T:d?k:(T+k)/2,y[_]=T,x[_]=k}return{vals:v,starts:y,ends:x}}},{"../../constants/numerical":479,"../../lib":503,"fast-isnumeric":190}],552:[function(t,e,r){"use strict";e.exports={xaxis:{valType:"subplotid",dflt:"x",editType:"calc+clearAxisTypes"},yaxis:{valType:"subplotid",dflt:"y",editType:"calc+clearAxisTypes"}}},{}],553:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("fast-isnumeric"),a=t("../../lib"),o=t("../../constants/numerical").FP_SAFE,s=t("../../registry"),l=t("../../components/drawing"),c=t("./axis_ids"),u=c.getFromId,f=c.isLinked;function h(t,e){var r,n,i=[],o=t._fullLayout,s=d(o,e,0),l=d(o,e,1),c=m(t,e),u=c.min,f=c.max;if(0===u.length||0===f.length)return a.simpleMap(e.range,e.r2l);var h=u[0].val,g=f[0].val;for(r=1;r0&&((T=E-s(x)-l(b))>L?k/T>C&&(_=x,w=b,C=k/T):k/E>C&&(_={val:x.val,nopad:1},w={val:b.val,nopad:1},C=k/E));if(h===g){var P=h-1,I=h+1;if(M)if(0===h)i=[0,1];else{var O=(h>0?f:u).reduce((function(t,e){return Math.max(t,l(e))}),0),z=h/(1-Math.min(.5,O/E));i=h>0?[0,z]:[z,0]}else i=S?[Math.max(0,P),Math.max(1,I)]:[P,I]}else M?(_.val>=0&&(_={val:0,nopad:1}),w.val<=0&&(w={val:0,nopad:1})):S&&(_.val-C*s(_)<0&&(_={val:0,nopad:1}),w.val<=0&&(w={val:1,nopad:1})),C=(w.val-_.val-p(e,x.val,b.val))/(E-s(_)-l(w)),i=[_.val-C*s(_),w.val+C*l(w)];return v&&i.reverse(),a.simpleMap(i,e.l2r||Number)}function p(t,e,r){var n=0;if(t.rangebreaks)for(var i=t.locateBreaks(e,r),a=0;a0?r.ppadplus:r.ppadminus)||r.ppad||0),S=A((t._m>0?r.ppadminus:r.ppadplus)||r.ppad||0),E=A(r.vpadplus||r.vpad),L=A(r.vpadminus||r.vpad);if(!T){if(h=1/0,p=-1/0,w)for(n=0;n0&&(h=a),a>p&&a-o&&(h=a),a>p&&a=I;n--)P(n);return{min:d,max:m,opts:r}},concatExtremes:m};function m(t,e,r){var n,i,a,o=e._id,s=t._fullData,l=t._fullLayout,c=[],f=[];function h(t,e){for(n=0;n=r&&(c.extrapad||!o)){s=!1;break}i(e,c.val)&&c.pad<=r&&(o||!c.extrapad)&&(t.splice(l,1),l--)}if(s){var u=a&&0===e;t.push({val:e,pad:u?0:r,extrapad:!u&&o})}}function x(t){return i(t)&&Math.abs(t)=e}},{"../../components/drawing":388,"../../constants/numerical":479,"../../lib":503,"../../registry":638,"./axis_ids":558,"@plotly/d3":58,"fast-isnumeric":190}],554:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("fast-isnumeric"),a=t("../../plots/plots"),o=t("../../registry"),s=t("../../lib"),l=s.strTranslate,c=t("../../lib/svg_text_utils"),u=t("../../components/titles"),f=t("../../components/color"),h=t("../../components/drawing"),p=t("./layout_attributes"),d=t("./clean_ticks"),m=t("../../constants/numerical"),g=m.ONEMAXYEAR,v=m.ONEAVGYEAR,y=m.ONEMINYEAR,x=m.ONEMAXQUARTER,b=m.ONEAVGQUARTER,_=m.ONEMINQUARTER,w=m.ONEMAXMONTH,T=m.ONEAVGMONTH,k=m.ONEMINMONTH,A=m.ONEWEEK,M=m.ONEDAY,S=M/2,E=m.ONEHOUR,L=m.ONEMIN,C=m.ONESEC,P=m.MINUS_SIGN,I=m.BADNUM,O={K:"zeroline"},z={K:"gridline",L:"path"},D={K:"minor-gridline",L:"path"},R={K:"tick",L:"path"},F={K:"tick",L:"text"},B=t("../../constants/alignment"),N=B.MID_SHIFT,j=B.CAP_SHIFT,U=B.LINE_SPACING,V=B.OPPOSITE_SIDE,H=e.exports={};H.setConvert=t("./set_convert");var q=t("./axis_autotype"),G=t("./axis_ids"),Y=G.idSort,W=G.isLinked;H.id2name=G.id2name,H.name2id=G.name2id,H.cleanId=G.cleanId,H.list=G.list,H.listIds=G.listIds,H.getFromId=G.getFromId,H.getFromTrace=G.getFromTrace;var X=t("./autorange");H.getAutoRange=X.getAutoRange,H.findExtremes=X.findExtremes;function Z(t){var e=1e-4*(t[1]-t[0]);return[t[0]-e,t[1]+e]}H.coerceRef=function(t,e,r,n,i,a){var o=n.charAt(n.length-1),l=r._fullLayout._subplots[o+"axis"],c=n+"ref",u={};return i||(i=l[0]||("string"==typeof a?a:a[0])),a||(a=i),l=l.concat(l.map((function(t){return t+" domain"}))),u[c]={valType:"enumerated",values:l.concat(a?"string"==typeof a?[a]:a:[]),dflt:i},s.coerce(t,e,u,c)},H.getRefType=function(t){return void 0===t?t:"paper"===t?"paper":"pixel"===t?"pixel":/( domain)$/.test(t)?"domain":"range"},H.coercePosition=function(t,e,r,n,i,a){var o,l;if("range"!==H.getRefType(n))o=s.ensureNumber,l=r(i,a);else{var c=H.getFromId(e,n);l=r(i,a=c.fraction2r(a)),o=c.cleanPos}t[i]=o(l)},H.cleanPosition=function(t,e,r){return("paper"===r||"pixel"===r?s.ensureNumber:H.getFromId(e,r).cleanPos)(t)},H.redrawComponents=function(t,e){e=e||H.listIds(t);var r=t._fullLayout;function n(n,i,a,s){for(var l=o.getComponentMethod(n,i),c={},u=0;ur&&f2e-6||((r-t._forceTick0)/t._minDtick%1+1.000001)%1>2e-6)&&(t._minDtick=0)):t._minDtick=0},H.saveRangeInitial=function(t,e){for(var r=H.list(t,"",!0),n=!1,i=0;i.3*h||u(n)||u(a))){var p=r.dtick/2;t+=t+p.8){var o=Number(r.substr(1));a.exactYears>.8&&o%12==0?t=H.tickIncrement(t,"M6","reverse")+1.5*M:a.exactMonths>.8?t=H.tickIncrement(t,"M1","reverse")+15.5*M:t-=S;var l=H.tickIncrement(t,r);if(l<=n)return l}return t}(y,t,v,c,a)),g=y,0;g<=u;)g=H.tickIncrement(g,v,!1,a);return{start:e.c2r(y,0,a),end:e.c2r(g,0,a),size:v,_dataSpan:u-c}},H.prepMinorTicks=function(t,e,r){if(!e.minor.dtick){delete t.dtick;var n,a=e.dtick&&i(e._tmin);if(a){var o=H.tickIncrement(e._tmin,e.dtick,!0);n=[e._tmin,.99*o+.01*e._tmin]}else{var l=s.simpleMap(e.range,e.r2l);n=[l[0],.8*l[0]+.2*l[1]]}if(t.range=s.simpleMap(n,e.l2r),t._isMinor=!0,H.prepTicks(t,r),a){var c=i(e.dtick),u=i(t.dtick),f=c?e.dtick:+e.dtick.substring(1),h=u?t.dtick:+t.dtick.substring(1);c&&u?$(f,h)?f===2*A&&h===2*M&&(t.dtick=A):f===2*A&&h===3*M?t.dtick=A:f!==A||(e._input.minor||{}).nticks?tt(f/h,2.5)?t.dtick=f/2:t.dtick=f:t.dtick=M:"M"===String(e.dtick).charAt(0)?u?t.dtick="M1":$(f,h)?f>=12&&2===h&&(t.dtick="M3"):t.dtick=e.dtick:"L"===String(t.dtick).charAt(0)?"L"===String(e.dtick).charAt(0)?$(f,h)||(t.dtick=tt(f/h,2.5)?e.dtick/2:e.dtick):t.dtick="D1":"D2"===t.dtick&&+e.dtick>1&&(t.dtick=1)}t.range=e.range}void 0===e.minor._tick0Init&&(t.tick0=e.tick0)},H.prepTicks=function(t,e){var r=s.simpleMap(t.range,t.r2l,void 0,void 0,e);if("auto"===t.tickmode||!t.dtick){var n,a=t.nticks;a||("category"===t.type||"multicategory"===t.type?(n=t.tickfont?s.bigFont(t.tickfont.size||12):15,a=t._length/n):(n="y"===t._id.charAt(0)?40:80,a=s.constrain(t._length/n,4,9)+1),"radialaxis"===t._name&&(a*=2)),t.minor&&"array"!==t.minor.tickmode||"array"===t.tickmode&&(a*=100),t._roughDTick=Math.abs(r[1]-r[0])/a,H.autoTicks(t,t._roughDTick),t._minDtick>0&&t.dtick<2*t._minDtick&&(t.dtick=t._minDtick,t.tick0=t.l2r(t._forceTick0))}"period"===t.ticklabelmode&&function(t){var e;function r(){return!(i(t.dtick)||"M"!==t.dtick.charAt(0))}var n=r(),a=H.getTickFormat(t);if(a){var o=t._dtickInit!==t.dtick;/%[fLQsSMX]/.test(a)||(/%[HI]/.test(a)?(e=E,o&&!n&&t.dtick=(O?0:1);z--){var D=!z;z?(t._dtickInit=t.dtick,t._tick0Init=t.tick0):(t.minor._dtickInit=t.minor.dtick,t.minor._tick0Init=t.minor.tick0);var R=z?t:s.extendFlat({},t,t.minor);if(D?H.prepMinorTicks(R,t,e):H.prepTicks(R,e),"array"!==R.tickmode){var F=Z(u),B=F[0],N=F[1],j=i(R.dtick),U="log"===a&&!(j||"L"===R.dtick.charAt(0)),V=H.tickFirst(R,e);if(z){if(t._tmin=V,V=N:W<=N;W=H.tickIncrement(W,X,f,o)){if(z&&q++,R.rangebreaks&&!f){if(W=p)break}if(C.length>d||W===Y)break;Y=W;var J={value:W};z?(U&&W!==(0|W)&&(J.simpleLabel=!0),l>1&&q%l&&(J.skipLabel=!0),C.push(J)):(J.minor=!0,P.push(J))}}else z?(C=[],m=rt(t)):(P=[],L=rt(t))}if(O&&!("inside"===t.minor.ticks&&"outside"===t.ticks||"outside"===t.minor.ticks&&"inside"===t.ticks)){for(var K=C.map((function(t){return t.value})),Q=[],$=0;$0?(a=n-1,o=n):(a=n,o=n);var s,l=t[a].value,c=t[o].value,u=Math.abs(c-l),f=r||u,h=0;f>=y?h=u>=y&&u<=g?u:v:r===b&&f>=_?h=u>=_&&u<=x?u:b:f>=k?h=u>=k&&u<=w?u:T:r===A&&f>=A?h=A:f>=M?h=M:r===S&&f>=S?h=S:r===E&&f>=E&&(h=E),h>=u&&(h=u,s=!0);var p=i+h;if(e.rangebreaks&&h>0){for(var d=0,m=0;m<84;m++){var L=(m+.5)/84;e.maskBreaks(i*(1-L)+L*p)!==I&&d++}(h*=d/84)||(t[n].drop=!0),s&&u>A&&(h=u)}(h>0||0===n)&&(t[n].periodX=i+h/2)}}(C,t,t._definedDelta),t.rangebreaks){var at="y"===t._id.charAt(0),ot=1;"auto"===t.tickmode&&(ot=t.tickfont?t.tickfont.size:12);var st=NaN;for(r=C.length-1;r>-1;r--)if(C[r].drop)C.splice(r,1);else{C[r].value=Ct(C[r].value,t);var lt=t.c2p(C[r].value);(at?st>lt-ot:stp||utp&&(ct.periodX=p),ut10||"01-01"!==n.substr(5)?t._tickround="d":t._tickround=+e.substr(1)%12==0?"y":"m";else if(e>=M&&a<=10||e>=15*M)t._tickround="d";else if(e>=L&&a<=16||e>=E)t._tickround="M";else if(e>=C&&a<=19||e>=L)t._tickround="S";else{var o=t.l2r(r+e).replace(/^-/,"").length;t._tickround=Math.max(a,o)-20,t._tickround<0&&(t._tickround=4)}}else if(i(e)||"L"===e.charAt(0)){var s=t.range.map(t.r2d||Number);i(e)||(e=Number(e.substr(1))),t._tickround=2-Math.floor(Math.log(e)/Math.LN10+.01);var l=Math.max(Math.abs(s[0]),Math.abs(s[1])),c=Math.floor(Math.log(l)/Math.LN10+.01),u=void 0===t.minexponent?3:t.minexponent;Math.abs(c)>u&&(dt(t.exponentformat)&&!mt(c)?t._tickexponent=3*Math.round((c-1)/3):t._tickexponent=c)}else t._tickround=null}function ht(t,e,r){var n=t.tickfont||{};return{x:e,dx:0,dy:0,text:r||"",fontSize:n.size,font:n.family,fontColor:n.color}}H.autoTicks=function(t,e,r){var n;function a(t){return Math.pow(t,Math.floor(Math.log(e)/Math.LN10))}if("date"===t.type){t.tick0=s.dateTick0(t.calendar,0);var o=2*e;if(o>v)e/=v,n=a(10),t.dtick="M"+12*ut(e,n,nt);else if(o>T)e/=T,t.dtick="M"+ut(e,1,it);else if(o>M){if(t.dtick=ut(e,M,t._hasDayOfWeekBreaks?[1,2,7,14]:ot),!r){var l=H.getTickFormat(t),c="period"===t.ticklabelmode;c&&(t._rawTick0=t.tick0),/%[uVW]/.test(l)?t.tick0=s.dateTick0(t.calendar,2):t.tick0=s.dateTick0(t.calendar,1),c&&(t._dowTick0=t.tick0)}}else o>E?t.dtick=ut(e,E,it):o>L?t.dtick=ut(e,L,at):o>C?t.dtick=ut(e,C,at):(n=a(10),t.dtick=ut(e,n,nt))}else if("log"===t.type){t.tick0=0;var u=s.simpleMap(t.range,t.r2l);if(t._isMinor&&(e*=1.5),e>.7)t.dtick=Math.ceil(e);else if(Math.abs(u[1]-u[0])<1){var f=1.5*Math.abs((u[1]-u[0])/e);e=Math.abs(Math.pow(10,u[1])-Math.pow(10,u[0]))/f,n=a(10),t.dtick="L"+ut(e,n,nt)}else t.dtick=e>.3?"D2":"D1"}else"category"===t.type||"multicategory"===t.type?(t.tick0=0,t.dtick=Math.ceil(Math.max(e,1))):Lt(t)?(t.tick0=0,n=1,t.dtick=ut(e,n,ct)):(t.tick0=0,n=a(10),t.dtick=ut(e,n,nt));if(0===t.dtick&&(t.dtick=1),!i(t.dtick)&&"string"!=typeof t.dtick){var h=t.dtick;throw t.dtick=1,"ax.dtick error: "+String(h)}},H.tickIncrement=function(t,e,r,a){var o=r?-1:1;if(i(e))return s.increment(t,o*e);var l=e.charAt(0),c=o*Number(e.substr(1));if("M"===l)return s.incrementMonth(t,c,a);if("L"===l)return Math.log(Math.pow(10,t)+c)/Math.LN10;if("D"===l){var u="D2"===e?lt:st,f=t+.01*o,h=s.roundUp(s.mod(f,1),u,r);return Math.floor(f)+Math.log(n.round(Math.pow(10,h),1))/Math.LN10}throw"unrecognized dtick "+String(e)},H.tickFirst=function(t,e){var r=t.r2l||Number,a=s.simpleMap(t.range,r,void 0,void 0,e),o=a[1] ")}else t._prevDateHead=l,c+="
"+l;e.text=c}(t,o,r,c):"log"===u?function(t,e,r,n,a){var o=t.dtick,l=e.x,c=t.tickformat,u="string"==typeof o&&o.charAt(0);"never"===a&&(a="");n&&"L"!==u&&(o="L3",u="L");if(c||"L"===u)e.text=gt(Math.pow(10,l),t,a,n);else if(i(o)||"D"===u&&s.mod(l+.01,1)<.1){var f=Math.round(l),h=Math.abs(f),p=t.exponentformat;"power"===p||dt(p)&&mt(f)?(e.text=0===f?1:1===f?"10":"10"+(f>1?"":P)+h+"",e.fontSize*=1.25):("e"===p||"E"===p)&&h>2?e.text="1"+p+(f>0?"+":P)+h:(e.text=gt(Math.pow(10,l),t,"","fakehover"),"D1"===o&&"y"===t._id.charAt(0)&&(e.dy-=e.fontSize/6))}else{if("D"!==u)throw"unrecognized dtick "+String(o);e.text=String(Math.round(Math.pow(10,s.mod(l,1)))),e.fontSize*=.75}if("D1"===t.dtick){var d=String(e.text).charAt(0);"0"!==d&&"1"!==d||("y"===t._id.charAt(0)?e.dx-=e.fontSize/4:(e.dy+=e.fontSize/2,e.dx+=(t.range[1]>t.range[0]?1:-1)*e.fontSize*(l<0?.5:.25)))}}(t,o,0,c,m):"category"===u?function(t,e){var r=t._categories[Math.round(e.x)];void 0===r&&(r="");e.text=String(r)}(t,o):"multicategory"===u?function(t,e,r){var n=Math.round(e.x),i=t._categories[n]||[],a=void 0===i[1]?"":String(i[1]),o=void 0===i[0]?"":String(i[0]);r?e.text=o+" - "+a:(e.text=a,e.text2=o)}(t,o,r):Lt(t)?function(t,e,r,n,i){if("radians"!==t.thetaunit||r)e.text=gt(e.x,t,i,n);else{var a=e.x/180;if(0===a)e.text="0";else{var o=function(t){function e(t,e){return Math.abs(t-e)<=1e-6}var r=function(t){for(var r=1;!e(Math.round(t*r)/r,t);)r*=10;return r}(t),n=t*r,i=Math.abs(function t(r,n){return e(n,0)?r:t(n,r%n)}(n,r));return[Math.round(n/i),Math.round(r/i)]}(a);if(o[1]>=100)e.text=gt(s.deg2rad(e.x),t,i,n);else{var l=e.x<0;1===o[1]?1===o[0]?e.text="\u03c0":e.text=o[0]+"\u03c0":e.text=["",o[0],"","\u2044","",o[1],"","\u03c0"].join(""),l&&(e.text=P+e.text)}}}}(t,o,r,c,m):function(t,e,r,n,i){"never"===i?i="":"all"===t.showexponent&&Math.abs(e.x/t.dtick)<1e-6&&(i="hide");e.text=gt(e.x,t,i,n)}(t,o,0,c,m),n||(t.tickprefix&&!d(t.showtickprefix)&&(o.text=t.tickprefix+o.text),t.ticksuffix&&!d(t.showticksuffix)&&(o.text+=t.ticksuffix)),"boundaries"===t.tickson||t.showdividers){var g=function(e){var r=t.l2p(e);return r>=0&&r<=t._length?e:null};o.xbnd=[g(o.x-.5),g(o.x+t.dtick-.5)]}return o},H.hoverLabelText=function(t,e,r){r&&(t=s.extendFlat({},t,{hoverformat:r}));var n=Array.isArray(e)?e[0]:e,i=Array.isArray(e)?e[1]:void 0;if(void 0!==i&&i!==n)return H.hoverLabelText(t,n,r)+" - "+H.hoverLabelText(t,i,r);var a="log"===t.type&&n<=0,o=H.tickText(t,t.c2l(a?-n:n),"hover").text;return a?0===n?"0":P+o:o};var pt=["f","p","n","\u03bc","m","","k","M","G","T"];function dt(t){return"SI"===t||"B"===t}function mt(t){return t>14||t<-15}function gt(t,e,r,n){var a=t<0,o=e._tickround,l=r||e.exponentformat||"B",c=e._tickexponent,u=H.getTickFormat(e),f=e.separatethousands;if(n){var h={exponentformat:l,minexponent:e.minexponent,dtick:"none"===e.showexponent?e.dtick:i(t)&&Math.abs(t)||1,range:"none"===e.showexponent?e.range.map(e.r2d):[0,t||1]};ft(h),o=(Number(h._tickround)||0)+4,c=h._tickexponent,e.hoverformat&&(u=e.hoverformat)}if(u)return e._numFormat(u)(t).replace(/-/g,P);var p,d=Math.pow(10,-o)/2;if("none"===l&&(c=0),(t=Math.abs(t))"+p+"":"B"===l&&9===c?t+="B":dt(l)&&(t+=pt[c/3+5]));return a?P+t:t}function vt(t,e){for(var r=[],n={},i=0;i1&&r=i.min&&t=0,a=u(t,e[1])<=0;return(r||i)&&(n||a)}if(t.tickformatstops&&t.tickformatstops.length>0)switch(t.type){case"date":case"linear":for(e=0;e=o(i)))){r=n;break}break;case"log":for(e=0;e0?r.bottom-f:0,h)))),e.automargin){n={x:0,y:0,r:0,l:0,t:0,b:0};var p=[0,1];if("x"===d){if("b"===l?n[l]=e._depth:(n[l]=e._depth=Math.max(r.width>0?f-r.top:0,h),p.reverse()),r.width>0){var g=r.right-(e._offset+e._length);g>0&&(n.xr=1,n.r=g);var v=e._offset-r.left;v>0&&(n.xl=0,n.l=v)}}else if("l"===l?n[l]=e._depth=Math.max(r.height>0?f-r.left:0,h):(n[l]=e._depth=Math.max(r.height>0?r.right-f:0,h),p.reverse()),r.height>0){var y=r.bottom-(e._offset+e._length);y>0&&(n.yb=0,n.b=y);var x=e._offset-r.top;x>0&&(n.yt=1,n.t=x)}n[m]="free"===e.anchor?e.position:e._anchorAxis.domain[p[0]],e.title.text!==c._dfltTitle[d]&&(n[l]+=bt(e)+(e.title.standoff||0)),e.mirror&&"free"!==e.anchor&&((i={x:0,y:0,r:0,l:0,t:0,b:0})[u]=e.linewidth,e.mirror&&!0!==e.mirror&&(i[u]+=h),!0===e.mirror||"ticks"===e.mirror?i[m]=e._anchorAxis.domain[p[1]]:"all"!==e.mirror&&"allticks"!==e.mirror||(i[m]=[e._counterDomainMin,e._counterDomainMax][p[1]]))}it&&(s=o.getComponentMethod("rangeslider","autoMarginOpts")(t,e)),a.autoMargin(t,Tt(e),n),a.autoMargin(t,kt(e),i),a.autoMargin(t,At(e),s)})),r.skipTitle||it&&"bottom"===e.side||rt.push((function(){return function(t,e){var r,n=t._fullLayout,i=e._id,a=i.charAt(0),o=e.title.font.size;if(e.title.hasOwnProperty("standoff"))r=e._depth+e.title.standoff+bt(e);else{var s=Pt(e);if("multicategory"===e.type)r=e._depth;else{var l=1.5*o;s&&(l=.5*o,"outside"===e.ticks&&(l+=e.ticklen)),r=10+l+(e.linewidth?e.linewidth-1:0)}s||(r+="x"===a?"top"===e.side?o*(e.showticklabels?1:0):o*(e.showticklabels?1.5:.5):"right"===e.side?o*(e.showticklabels?1:.5):o*(e.showticklabels?.5:0))}var c,f,p,d,m=H.getPxPosition(t,e);"x"===a?(f=e._offset+e._length/2,p="top"===e.side?m-r:m+r):(p=e._offset+e._length/2,f="right"===e.side?m+r:m-r,c={rotate:"-90",offset:0});if("multicategory"!==e.type){var g=e._selections[e._id+"tick"];if(d={selection:g,side:e.side},g&&g.node()&&g.node().parentNode){var v=h.getTranslate(g.node().parentNode);d.offsetLeft=v.x,d.offsetTop=v.y}e.title.hasOwnProperty("standoff")&&(d.pad=0)}return u.draw(t,i+"title",{propContainer:e,propName:e._name+".title.text",placeholder:n._dfltTitle[a],avoid:d,transform:c,attributes:{x:f,y:p,"text-anchor":"middle"}})}(t,e)})),s.syncOrAsync(rt)}}function at(t){var r=p+(t||"tick");return w[r]||(w[r]=function(t,e){var r,n,i,a;t._selections[e].size()?(r=1/0,n=-1/0,i=1/0,a=-1/0,t._selections[e].each((function(){var t=wt(this),e=h.bBox(t.node().parentNode);r=Math.min(r,e.top),n=Math.max(n,e.bottom),i=Math.min(i,e.left),a=Math.max(a,e.right)}))):(r=0,n=0,i=0,a=0);return{top:r,bottom:n,left:i,right:a,height:n-r,width:a-i}}(e,r)),w[r]}},H.getTickSigns=function(t,e){var r=t._id.charAt(0),n={x:"top",y:"right"}[r],i=t.side===n?1:-1,a=[-1,1,i,-i];return"inside"!==(e?(t.minor||{}).ticks:t.ticks)==("x"===r)&&(a=a.map((function(t){return-t}))),t.side&&a.push({l:-1,t:-1,r:1,b:1}[t.side.charAt(0)]),a},H.makeTransTickFn=function(t){return"x"===t._id.charAt(0)?function(e){return l(t._offset+t.l2p(e.x),0)}:function(e){return l(0,t._offset+t.l2p(e.x))}},H.makeTransTickLabelFn=function(t){var e=function(t){var e=t.ticklabelposition||"",r=function(t){return-1!==e.indexOf(t)},n=r("top"),i=r("left"),a=r("right"),o=r("bottom"),s=r("inside"),l=o||i||n||a;if(!l&&!s)return[0,0];var c=t.side,u=l?(t.tickwidth||0)/2:0,f=3,h=t.tickfont?t.tickfont.size:12;(o||n)&&(u+=h*j,f+=(t.linewidth||0)/2);(i||a)&&(u+=(t.linewidth||0)/2,f+=3);s&&"top"===c&&(f-=h*(1-j));(i||n)&&(u=-u);"bottom"!==c&&"right"!==c||(f=-f);return[l?u:0,s?f:0]}(t),r=e[0],n=e[1];return"x"===t._id.charAt(0)?function(e){return l(r+t._offset+t.l2p(yt(e)),n)}:function(e){return l(n,r+t._offset+t.l2p(yt(e)))}},H.makeTickPath=function(t,e,r,n){n||(n={});var i=n.minor;if(i&&!t.minor)return"";var a=void 0!==n.len?n.len:i?t.minor.ticklen:t.ticklen,o=t._id.charAt(0),s=(t.linewidth||1)/2;return"x"===o?"M0,"+(e+s*r)+"v"+a*r:"M"+(e+s*r)+",0h"+a*r},H.makeLabelFns=function(t,e,r){var n=t.ticklabelposition||"",a=function(t){return-1!==n.indexOf(t)},o=a("top"),l=a("left"),c=a("right"),u=a("bottom")||l||o||c,f=a("inside"),h="inside"===n&&"inside"===t.ticks||!f&&"outside"===t.ticks&&"boundaries"!==t.tickson,p=0,d=0,m=h?t.ticklen:0;if(f?m*=-1:u&&(m=0),h&&(p+=m,r)){var g=s.deg2rad(r);p=m*Math.cos(g)+1,d=m*Math.sin(g)}t.showticklabels&&(h||t.showline)&&(p+=.2*t.tickfont.size);var v,y,x,b,_,w={labelStandoff:p+=(t.linewidth||1)/2*(f?-1:1),labelShift:d},T=0,k=t.side,A=t._id.charAt(0),M=t.tickangle;if("x"===A)b=(_=!f&&"bottom"===k||f&&"top"===k)?1:-1,f&&(b*=-1),v=d*b,y=e+p*b,x=_?1:-.2,90===Math.abs(M)&&(f?x+=N:x=-90===M&&"bottom"===k?j:90===M&&"top"===k?N:.5,T=N/2*(M/90)),w.xFn=function(t){return t.dx+v+T*t.fontSize},w.yFn=function(t){return t.dy+y+t.fontSize*x},w.anchorFn=function(t,e){if(u){if(l)return"end";if(c)return"start"}return i(e)&&0!==e&&180!==e?e*b<0!==f?"end":"start":"middle"},w.heightFn=function(e,r,n){return r<-60||r>60?-.5*n:"top"===t.side!==f?-n:0};else if("y"===A){if(b=(_=!f&&"left"===k||f&&"right"===k)?1:-1,f&&(b*=-1),v=p,y=d*b,x=0,f||90!==Math.abs(M)||(x=-90===M&&"left"===k||90===M&&"right"===k?j:.5),f){var S=i(M)?+M:0;if(0!==S){var E=s.deg2rad(S);T=Math.abs(Math.sin(E))*j*b,x=0}}w.xFn=function(t){return t.dx+e-(v+t.fontSize*x)*b+T*t.fontSize},w.yFn=function(t){return t.dy+y+t.fontSize*N},w.anchorFn=function(t,e){return i(e)&&90===Math.abs(e)?"middle":_?"end":"start"},w.heightFn=function(e,r,n){return"right"===t.side&&(r*=-1),r<-30?-n:r<30?-.5*n:0}}return w},H.drawTicks=function(t,e,r){r=r||{};var i=e._id+"tick",a=[].concat(e.minor&&e.minor.ticks?r.vals.filter((function(t){return t.minor&&!t.noTick})):[]).concat(e.ticks?r.vals.filter((function(t){return!t.minor&&!t.noTick})):[]),o=r.layer.selectAll("path."+i).data(a,xt);o.exit().remove(),o.enter().append("path").classed(i,1).classed("ticks",1).classed("crisp",!1!==r.crisp).each((function(t){return f.stroke(n.select(this),t.minor?e.minor.tickcolor:e.tickcolor)})).style("stroke-width",(function(r){return h.crispRound(t,r.minor?e.minor.tickwidth:e.tickwidth,1)+"px"})).attr("d",r.path).style("display",null),It(e,[R]),o.attr("transform",r.transFn)},H.drawGrid=function(t,e,r){r=r||{};var i=e._id+"grid",a=e.minor&&e.minor.showgrid,o=a?r.vals.filter((function(t){return t.minor})):[],s=e.showgrid?r.vals.filter((function(t){return!t.minor})):[],l=r.counterAxis;if(l&&H.shouldShowZeroLine(t,e,l))for(var c="array"===e.tickmode,u=0;u=0;v--){var y=v?m:g;if(y){var x=y.selectAll("path."+i).data(v?s:o,xt);x.exit().remove(),x.enter().append("path").classed(i,1).classed("crisp",!1!==r.crisp),x.attr("transform",r.transFn).attr("d",r.path).each((function(t){return f.stroke(n.select(this),t.minor?e.minor.gridcolor:e.gridcolor||"#ddd")})).style("stroke-dasharray",(function(t){return h.dashStyle(t.minor?e.minor.griddash:e.griddash,t.minor?e.minor.gridwidth:e.gridwidth)})).style("stroke-width",(function(t){return(t.minor?d:e._gw)+"px"})).style("display",null),"function"==typeof r.path&&x.attr("d",r.path)}}It(e,[z,D])},H.drawZeroLine=function(t,e,r){r=r||r;var n=e._id+"zl",i=H.shouldShowZeroLine(t,e,r.counterAxis),a=r.layer.selectAll("path."+n).data(i?[{x:0,id:e._id}]:[]);a.exit().remove(),a.enter().append("path").classed(n,1).classed("zl",1).classed("crisp",!1!==r.crisp).each((function(){r.layer.selectAll("path").sort((function(t,e){return Y(t.id,e.id)}))})),a.attr("transform",r.transFn).attr("d",r.path).call(f.stroke,e.zerolinecolor||f.defaultLine).style("stroke-width",h.crispRound(t,e.zerolinewidth,e._gw||1)+"px").style("display",null),It(e,[O])},H.drawLabels=function(t,e,r){r=r||{};var a=t._fullLayout,o=e._id,u=o.charAt(0),f=r.cls||o+"tick",p=r.vals.filter((function(t){return t.text})),d=r.labelFns,m=r.secondary?0:e.tickangle,g=(e._prevTickAngles||{})[f],v=r.layer.selectAll("g."+f).data(e.showticklabels?p:[],xt),y=[];function x(t,a){t.each((function(t){var o=n.select(this),s=o.select(".text-math-group"),u=d.anchorFn(t,a),f=r.transFn.call(o.node(),t)+(i(a)&&0!=+a?" rotate("+a+","+d.xFn(t)+","+(d.yFn(t)-t.fontSize/2)+")":""),p=c.lineCount(o),m=U*t.fontSize,g=d.heightFn(t,i(a)?+a:0,(p-1)*m);if(g&&(f+=l(0,g)),s.empty()){var v=o.select("text");v.attr({transform:f,"text-anchor":u}),v.style("opacity",1),e._adjustTickLabelsOverflow&&e._adjustTickLabelsOverflow()}else{var y=h.bBox(s.node()).width*{end:-.5,start:.5}[u];s.attr("transform",f+l(y,0))}}))}v.enter().append("g").classed(f,1).append("text").attr("text-anchor","middle").each((function(e){var r=n.select(this),i=t._promises.length;r.call(c.positionText,d.xFn(e),d.yFn(e)).call(h.font,e.font,e.fontSize,e.fontColor).text(e.text).call(c.convertToTspans,t),t._promises[i]?y.push(t._promises.pop().then((function(){x(r,m)}))):x(r,m)})),It(e,[F]),v.exit().remove(),r.repositionOnUpdate&&v.each((function(t){n.select(this).select("text").call(c.positionText,d.xFn(t),d.yFn(t))})),e._adjustTickLabelsOverflow=function(){var r=e.ticklabeloverflow;if(r&&"allow"!==r){var i=-1!==r.indexOf("hide"),o="x"===e._id.charAt(0),l=0,c=o?t._fullLayout.width:t._fullLayout.height;if(-1!==r.indexOf("domain")){var u=s.simpleMap(e.range,e.r2l);l=e.l2p(u[0])+e._offset,c=e.l2p(u[1])+e._offset}var f=Math.min(l,c),p=Math.max(l,c),d=e.side,m=1/0,g=-1/0;for(var y in v.each((function(t){var r=n.select(this);if(r.select(".text-math-group").empty()){var a=h.bBox(r.node()),s=0;o?(a.right>p||a.leftp||a.top+(e.tickangle?0:t.fontSize/4)e["_visibleLabelMin_"+r._id]?l.style("display","none"):"tick"!==t.K||i||l.style("display",null)}))}))}))}))},x(v,g+1?g:m);var b=null;e._selections&&(e._selections[f]=v);var _=[function(){return y.length&&Promise.all(y)}];e.automargin&&a._redrawFromAutoMarginCount&&90===g?(b=90,_.push((function(){x(v,g)}))):_.push((function(){if(x(v,m),p.length&&"x"===u&&!i(m)&&("log"!==e.type||"D"!==String(e.dtick).charAt(0))){b=0;var t,n=0,a=[];if(v.each((function(t){n=Math.max(n,t.fontSize);var r=e.l2p(t.x),i=wt(this),o=h.bBox(i.node());a.push({top:0,bottom:10,height:10,left:r-o.width/2,right:r+o.width/2+2,width:o.width+2})})),"boundaries"!==e.tickson&&!e.showdividers||r.secondary){var o=p.length,l=Math.abs((p[o-1].x-p[0].x)*e._m)/(o-1),c=e.ticklabelposition||"",f=function(t){return-1!==c.indexOf(t)},d=f("top"),g=f("left"),y=f("right"),_=f("bottom")||g||d||y?(e.tickwidth||0)+6:0,w=l<2.5*n||"multicategory"===e.type||"realaxis"===e._name;for(t=0;t1)for(n=1;n2*o}(i,e))return"date";var g="strict"!==r.autotypenumbers;return function(t,e){for(var r=t.length,n=f(r),i=0,o=0,s={},u=0;u2*i}(i,g)?"category":function(t,e){for(var r=t.length,n=0;n=2){var s,c,u="";if(2===o.length)for(s=0;s<2;s++)if(c=b(o[s])){u=g;break}var f=i("pattern",u);if(f===g)for(s=0;s<2;s++)(c=b(o[s]))&&(e.bounds[s]=o[s]=c-1);if(f)for(s=0;s<2;s++)switch(c=o[s],f){case g:if(!n(c))return void(e.enabled=!1);if((c=+c)!==Math.floor(c)||c<0||c>=7)return void(e.enabled=!1);e.bounds[s]=o[s]=c;break;case v:if(!n(c))return void(e.enabled=!1);if((c=+c)<0||c>24)return void(e.enabled=!1);e.bounds[s]=o[s]=c}if(!1===r.autorange){var h=r.range;if(h[0]h[1])return void(e.enabled=!1)}else if(o[0]>h[0]&&o[1]n?1:-1:+(t.substr(1)||1)-+(e.substr(1)||1)},r.ref2id=function(t){return!!/^[xyz]/.test(t)&&t.split(" ")[0]},r.isLinked=function(t,e){return a(e,t._axisMatchGroups)||a(e,t._axisConstraintGroups)}},{"../../registry":638,"./constants":561}],559:[function(t,e,r){"use strict";e.exports=function(t,e,r,n){if("category"===e.type){var i,a=t.categoryarray,o=Array.isArray(a)&&a.length>0;o&&(i="array");var s,l=r("categoryorder",i);"array"===l&&(s=r("categoryarray")),o||"array"!==l||(l=e.categoryorder="trace"),"trace"===l?e._initialCategories=[]:"array"===l?e._initialCategories=s.slice():(s=function(t,e){var r,n,i,a=e.dataAttr||t._id.charAt(0),o={};if(e.axData)r=e.axData;else for(r=[],n=0;nn?i.substr(n):a.substr(r))+o:i+a+t*e:o}function g(t,e){for(var r=e._size,n=r.h/r.w,i={},a=Object.keys(t),o=0;oc*x)||T)for(r=0;rO&&FP&&(P=F);h/=(P-C)/(2*I),C=l.l2r(C),P=l.l2r(P),l.range=l._input.range=S=0?Math.min(t,.9):1/(1/Math.max(t,-.3)+3.222))}function N(t,e,r,n,i){return t.append("path").attr("class","zoombox").style({fill:e>.2?"rgba(0,0,0,0)":"rgba(255,255,255,0)","stroke-width":0}).attr("transform",c(r,n)).attr("d",i+"Z")}function j(t,e,r){return t.append("path").attr("class","zoombox-corners").style({fill:f.background,stroke:f.defaultLine,"stroke-width":1,opacity:0}).attr("transform",c(e,r)).attr("d","M0,0Z")}function U(t,e,r,n,i,a){t.attr("d",n+"M"+r.l+","+r.t+"v"+r.h+"h"+r.w+"v-"+r.h+"h-"+r.w+"Z"),V(t,e,i,a)}function V(t,e,r,n){r||(t.transition().style("fill",n>.2?"rgba(0,0,0,0.4)":"rgba(255,255,255,0.3)").duration(200),e.transition().style("opacity",1).duration(200))}function H(t){n.select(t).selectAll(".zoombox,.js-zoombox-backdrop,.js-zoombox-menu,.zoombox-corners").remove()}function q(t){I&&t.data&&t._context.showTips&&(i.notifier(i._(t,"Double-click to zoom back out"),"long"),I=!1)}function G(t){var e=Math.floor(Math.min(t.b-t.t,t.r-t.l,P)/2);return"M"+(t.l-3.5)+","+(t.t-.5+e)+"h3v"+-e+"h"+e+"v-3h-"+(e+3)+"ZM"+(t.r+3.5)+","+(t.t-.5+e)+"h-3v"+-e+"h"+-e+"v-3h"+(e+3)+"ZM"+(t.r+3.5)+","+(t.b+.5-e)+"h-3v"+e+"h"+-e+"v3h"+(e+3)+"ZM"+(t.l-3.5)+","+(t.b+.5-e)+"h3v"+e+"h"+e+"v3h-"+(e+3)+"Z"}function Y(t,e,r,n,a){for(var o,s,l,c,u=!1,f={},h={},p=(a||{}).xaHash,d=(a||{}).yaHash,m=0;m=0)i._fullLayout._deactivateShape(i);else{var o=i._fullLayout.clickmode;if(H(i),2!==t||vt||qt(),gt)o.indexOf("select")>-1&&S(r,i,J,K,e.id,Pt),o.indexOf("event")>-1&&p.click(i,r,e.id);else if(1===t&&vt){var s=m?O:I,c="s"===m||"w"===v?0:1,f=s._name+".range["+c+"]",h=function(t,e){var r,n=t.range[e],i=Math.abs(n-t.range[1-e]);return"date"===t.type?n:"log"===t.type?(r=Math.ceil(Math.max(0,-Math.log(i)/Math.LN10))+3,a("."+r+"g")(Math.pow(10,n))):(r=Math.floor(Math.log(Math.abs(n))/Math.LN10)-Math.floor(Math.log(i)/Math.LN10)+4,a("."+String(r)+"g")(n))}(s,c),d="left",g="middle";if(s.fixedrange)return;m?(g="n"===m?"top":"bottom","right"===s.side&&(d="right")):"e"===v&&(d="right"),i._context.showAxisRangeEntryBoxes&&n.select(bt).call(u.makeEditable,{gd:i,immediate:!0,background:i._fullLayout.paper_bgcolor,text:String(h),fill:s.tickfont?s.tickfont.color:"#444",horizontalAlign:d,verticalAlign:g}).on("edit",(function(t){var e=s.d2r(t);void 0!==e&&l.call("_guiRelayout",i,f,e)}))}}}function zt(e,r){if(t._transitioningWithDuration)return!1;var n=Math.max(0,Math.min(tt,pt*e+_t)),i=Math.max(0,Math.min(et,dt*r+wt)),a=Math.abs(n-_t),o=Math.abs(i-wt);function s(){St="",Tt.r=Tt.l,Tt.t=Tt.b,Lt.attr("d","M0,0Z")}if(Tt.l=Math.min(_t,n),Tt.r=Math.max(_t,n),Tt.t=Math.min(wt,i),Tt.b=Math.max(wt,i),rt.isSubplotConstrained)a>P||o>P?(St="xy",a/tt>o/et?(o=a*et/tt,wt>i?Tt.t=wt-o:Tt.b=wt+o):(a=o*tt/et,_t>n?Tt.l=_t-a:Tt.r=_t+a),Lt.attr("d",G(Tt))):s();else if(nt.isSubplotConstrained)if(a>P||o>P){St="xy";var l=Math.min(Tt.l/tt,(et-Tt.b)/et),c=Math.max(Tt.r/tt,(et-Tt.t)/et);Tt.l=l*tt,Tt.r=c*tt,Tt.b=(1-l)*et,Tt.t=(1-c)*et,Lt.attr("d",G(Tt))}else s();else!at||o0){var u;if(nt.isSubplotConstrained||!it&&1===at.length){for(u=0;um[1]-1/4096&&(e.domain=s),i.noneOrAll(t.domain,e.domain,s)}return r("layer"),e}},{"../../lib":503,"fast-isnumeric":190}],573:[function(t,e,r){"use strict";var n=t("./show_dflt");e.exports=function(t,e,r,i,a){a||(a={});var o=a.tickSuffixDflt,s=n(t);r("tickprefix")&&r("showtickprefix",s),r("ticksuffix",o)&&r("showticksuffix",s)}},{"./show_dflt":577}],574:[function(t,e,r){"use strict";var n=t("../../constants/alignment").FROM_BL;e.exports=function(t,e,r){void 0===r&&(r=n[t.constraintoward||"center"]);var i=[t.r2l(t.range[0]),t.r2l(t.range[1])],a=i[0]+(i[1]-i[0])*r;t.range=t._input.range=[t.l2r(a+(i[0]-a)*e),t.l2r(a+(i[1]-a)*e)],t.setScale()}},{"../../constants/alignment":471}],575:[function(t,e,r){"use strict";var n=t("polybooljs"),i=t("../../registry"),a=t("../../components/drawing").dashStyle,o=t("../../components/color"),s=t("../../components/fx"),l=t("../../components/fx/helpers").makeEventData,c=t("../../components/dragelement/helpers"),u=c.freeMode,f=c.rectMode,h=c.drawMode,p=c.openMode,d=c.selectMode,m=t("../../components/shapes/draw_newshape/display_outlines"),g=t("../../components/shapes/draw_newshape/helpers").handleEllipse,v=t("../../components/shapes/draw_newshape/newshapes"),y=t("../../lib"),x=t("../../lib/polygon"),b=t("../../lib/throttle"),_=t("./axis_ids").getFromId,w=t("../../lib/clear_gl_canvases"),T=t("../../plot_api/subroutines").redrawReglTraces,k=t("./constants"),A=k.MINSELECT,M=x.filter,S=x.tester,E=t("./handle_outline").clearSelect,L=t("./helpers"),C=L.p2r,P=L.axValue,I=L.getTransform;function O(t,e,r,n,i,a,o){var s,l,c,u,f,h,d,g,v,y=e._hoverdata,x=e._fullLayout.clickmode.indexOf("event")>-1,b=[];if(function(t){return t&&Array.isArray(t)&&!0!==t[0].hoverOnBox}(y)){F(t,e,a);var _=function(t,e){var r,n,i=t[0],a=-1,o=[];for(n=0;n0?function(t,e){var r,n,i,a=[];for(i=0;i0&&a.push(r);if(1===a.length&&a[0]===e.searchInfo&&(n=e.searchInfo.cd[0].trace).selectedpoints.length===e.pointNumbers.length){for(i=0;i1)return!1;if((i+=r.selectedpoints.length)>1)return!1}return 1===i}(s)&&(h=j(_))){for(o&&o.remove(),v=0;v=0&&n._fullLayout._deactivateShape(n),h(e)){var a=n._fullLayout._zoomlayer.selectAll(".select-outline-"+r.id);if(a&&n._fullLayout._drawing){var o=v(a,t);o&&i.call("_guiRelayout",n,{shapes:o}),n._fullLayout._drawing=!1}}r.selection={},r.selection.selectionDefs=t.selectionDefs=[],r.selection.mergedPolygons=t.mergedPolygons=[]}function N(t,e,r,n){var i,a,o,s=[],l=e.map((function(t){return t._id})),c=r.map((function(t){return t._id}));for(o=0;o0?n[0]:r;return!!e.selectedpoints&&e.selectedpoints.indexOf(i)>-1}function U(t,e,r){var n,a,o,s;for(n=0;n=0)L._fullLayout._deactivateShape(L);else if(!_){var r=z.clickmode;b.done(mt).then((function(){if(b.clear(mt),2===t){for(ft.remove(),$=0;$-1&&O(e,L,i.xaxes,i.yaxes,i.subplot,i,ft),"event"===r&&L.emit("plotly_selected",void 0);s.click(L,e)})).catch(y.error)}},i.doneFn=function(){dt.remove(),b.done(mt).then((function(){b.clear(mt),i.gd.emit("plotly_selected",et),Q&&i.selectionDefs&&(Q.subtract=ut,i.selectionDefs.push(Q),i.mergedPolygons.length=0,[].push.apply(i.mergedPolygons,K)),i.doneFnCompleted&&i.doneFnCompleted(gt)})).catch(y.error),_&&B(i)}},clearSelect:E,clearSelectionsCache:B,selectOnClick:O}},{"../../components/color":366,"../../components/dragelement/helpers":384,"../../components/drawing":388,"../../components/fx":406,"../../components/fx/helpers":402,"../../components/shapes/draw_newshape/display_outlines":454,"../../components/shapes/draw_newshape/helpers":455,"../../components/shapes/draw_newshape/newshapes":456,"../../lib":503,"../../lib/clear_gl_canvases":487,"../../lib/polygon":515,"../../lib/throttle":530,"../../plot_api/subroutines":544,"../../registry":638,"./axis_ids":558,"./constants":561,"./handle_outline":565,"./helpers":566,polybooljs:254}],576:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("d3-time-format").utcFormat,a=t("../../lib"),o=a.numberFormat,s=t("fast-isnumeric"),l=a.cleanNumber,c=a.ms2DateTime,u=a.dateTime2ms,f=a.ensureNumber,h=a.isArrayOrTypedArray,p=t("../../constants/numerical"),d=p.FP_SAFE,m=p.BADNUM,g=p.LOG_CLIP,v=p.ONEWEEK,y=p.ONEDAY,x=p.ONEHOUR,b=p.ONEMIN,_=p.ONESEC,w=t("./axis_ids"),T=t("./constants"),k=T.HOUR_PATTERN,A=T.WEEKDAY_PATTERN;function M(t){return Math.pow(10,t)}function S(t){return null!=t}e.exports=function(t,e){e=e||{};var r=t._id||"x",p=r.charAt(0);function E(e,r){if(e>0)return Math.log(e)/Math.LN10;if(e<=0&&r&&t.range&&2===t.range.length){var n=t.range[0],i=t.range[1];return.5*(n+i-2*g*Math.abs(n-i))}return m}function L(e,r,n,i){if((i||{}).msUTC&&s(e))return+e;var o=u(e,n||t.calendar);if(o===m){if(!s(e))return m;e=+e;var l=Math.floor(10*a.mod(e+.05,1)),c=Math.round(e-l/10);o=u(new Date(c))+l/10}return o}function C(e,r,n){return c(e,r,n||t.calendar)}function P(e){return t._categories[Math.round(e)]}function I(e){if(S(e)){if(void 0===t._categoriesMap&&(t._categoriesMap={}),void 0!==t._categoriesMap[e])return t._categoriesMap[e];t._categories.push("number"==typeof e?String(e):e);var r=t._categories.length-1;return t._categoriesMap[e]=r,r}return m}function O(e){if(t._categoriesMap)return t._categoriesMap[e]}function z(t){var e=O(t);return void 0!==e?e:s(t)?+t:void 0}function D(t){return s(t)?+t:O(t)}function R(t,e,r){return n.round(r+e*t,2)}function F(t,e,r){return(t-r)/e}var B=function(e){return s(e)?R(e,t._m,t._b):m},N=function(e){return F(e,t._m,t._b)};if(t.rangebreaks){var j="y"===p;B=function(e){if(!s(e))return m;var r=t._rangebreaks.length;if(!r)return R(e,t._m,t._b);var n=j;t.range[0]>t.range[1]&&(n=!n);for(var i=n?-1:1,a=i*e,o=0,l=0;lu)){o=a<(c+u)/2?l:l+1;break}o=l+1}var f=t._B[o]||0;return isFinite(f)?R(e,t._m2,f):0},N=function(e){var r=t._rangebreaks.length;if(!r)return F(e,t._m,t._b);for(var n=0,i=0;it._rangebreaks[i].pmax&&(n=i+1);return F(e,t._m2,t._B[n])}}t.c2l="log"===t.type?E:f,t.l2c="log"===t.type?M:f,t.l2p=B,t.p2l=N,t.c2p="log"===t.type?function(t,e){return B(E(t,e))}:B,t.p2c="log"===t.type?function(t){return M(N(t))}:N,-1!==["linear","-"].indexOf(t.type)?(t.d2r=t.r2d=t.d2c=t.r2c=t.d2l=t.r2l=l,t.c2d=t.c2r=t.l2d=t.l2r=f,t.d2p=t.r2p=function(e){return t.l2p(l(e))},t.p2d=t.p2r=N,t.cleanPos=f):"log"===t.type?(t.d2r=t.d2l=function(t,e){return E(l(t),e)},t.r2d=t.r2c=function(t){return M(l(t))},t.d2c=t.r2l=l,t.c2d=t.l2r=f,t.c2r=E,t.l2d=M,t.d2p=function(e,r){return t.l2p(t.d2r(e,r))},t.p2d=function(t){return M(N(t))},t.r2p=function(e){return t.l2p(l(e))},t.p2r=N,t.cleanPos=f):"date"===t.type?(t.d2r=t.r2d=a.identity,t.d2c=t.r2c=t.d2l=t.r2l=L,t.c2d=t.c2r=t.l2d=t.l2r=C,t.d2p=t.r2p=function(e,r,n){return t.l2p(L(e,0,n))},t.p2d=t.p2r=function(t,e,r){return C(N(t),e,r)},t.cleanPos=function(e){return a.cleanDate(e,m,t.calendar)}):"category"===t.type?(t.d2c=t.d2l=I,t.r2d=t.c2d=t.l2d=P,t.d2r=t.d2l_noadd=z,t.r2c=function(e){var r=D(e);return void 0!==r?r:t.fraction2r(.5)},t.l2r=t.c2r=f,t.r2l=D,t.d2p=function(e){return t.l2p(t.r2c(e))},t.p2d=function(t){return P(N(t))},t.r2p=t.d2p,t.p2r=N,t.cleanPos=function(t){return"string"==typeof t&&""!==t?t:f(t)}):"multicategory"===t.type&&(t.r2d=t.c2d=t.l2d=P,t.d2r=t.d2l_noadd=z,t.r2c=function(e){var r=z(e);return void 0!==r?r:t.fraction2r(.5)},t.r2c_just_indices=O,t.l2r=t.c2r=f,t.r2l=z,t.d2p=function(e){return t.l2p(t.r2c(e))},t.p2d=function(t){return P(N(t))},t.r2p=t.d2p,t.p2r=N,t.cleanPos=function(t){return Array.isArray(t)||"string"==typeof t&&""!==t?t:f(t)},t.setupMultiCategory=function(n){var i,o,s=t._traceIndices,l=t._matchGroup;if(l&&0===t._categories.length)for(var c in l)if(c!==r){var u=e[w.id2name(c)];s=s.concat(u._traceIndices)}var f=[[0,{}],[0,{}]],d=[];for(i=0;id&&(o[n]=d),o[0]===o[1]){var c=Math.max(1,Math.abs(1e-6*o[0]));o[0]-=c,o[1]+=c}}else a.nestedProperty(t,e).set(i)},t.setScale=function(r){var n=e._size;if(t.overlaying){var i=w.getFromId({_fullLayout:e},t.overlaying);t.domain=i.domain}var a=r&&t._r?"_r":"range",o=t.calendar;t.cleanRange(a);var s,l,c=t.r2l(t[a][0],o),u=t.r2l(t[a][1],o),f="y"===p;if((f?(t._offset=n.t+(1-t.domain[1])*n.h,t._length=n.h*(t.domain[1]-t.domain[0]),t._m=t._length/(c-u),t._b=-t._m*u):(t._offset=n.l+t.domain[0]*n.w,t._length=n.w*(t.domain[1]-t.domain[0]),t._m=t._length/(u-c),t._b=-t._m*c),t._rangebreaks=[],t._lBreaks=0,t._m2=0,t._B=[],t.rangebreaks)&&(t._rangebreaks=t.locateBreaks(Math.min(c,u),Math.max(c,u)),t._rangebreaks.length)){for(s=0;su&&(h=!h),h&&t._rangebreaks.reverse();var d=h?-1:1;for(t._m2=d*t._length/(Math.abs(u-c)-t._lBreaks),t._B.push(-t._m2*(f?u:c)),s=0;si&&(i+=7,oi&&(i+=24,o=n&&o=n&&e=s.min&&(ts.max&&(s.max=n),i=!1)}i&&c.push({min:t,max:n})}};for(n=0;nr.duration?(!function(){for(var r={},n=0;n rect").call(o.setTranslate,0,0).call(o.setScale,1,1),t.plot.call(o.setTranslate,e._offset,r._offset).call(o.setScale,1,1);var n=t.plot.selectAll(".scatterlayer .trace");n.selectAll(".point").call(o.setPointGroupScale,1,1),n.selectAll(".textpoint").call(o.setTextPointsScale,1,1),n.call(o.hideOutsideRangePoints,t)}function g(e,r){var n=e.plotinfo,i=n.xaxis,l=n.yaxis,c=i._length,u=l._length,f=!!e.xr1,h=!!e.yr1,p=[];if(f){var d=a.simpleMap(e.xr0,i.r2l),m=a.simpleMap(e.xr1,i.r2l),g=d[1]-d[0],v=m[1]-m[0];p[0]=(d[0]*(1-r)+r*m[0]-d[0])/(d[1]-d[0])*c,p[2]=c*(1-r+r*v/g),i.range[0]=i.l2r(d[0]*(1-r)+r*m[0]),i.range[1]=i.l2r(d[1]*(1-r)+r*m[1])}else p[0]=0,p[2]=c;if(h){var y=a.simpleMap(e.yr0,l.r2l),x=a.simpleMap(e.yr1,l.r2l),b=y[1]-y[0],_=x[1]-x[0];p[1]=(y[1]*(1-r)+r*x[1]-y[1])/(y[0]-y[1])*u,p[3]=u*(1-r+r*_/b),l.range[0]=i.l2r(y[0]*(1-r)+r*x[0]),l.range[1]=l.l2r(y[1]*(1-r)+r*x[1])}else p[1]=0,p[3]=u;s.drawOne(t,i,{skipTitle:!0}),s.drawOne(t,l,{skipTitle:!0}),s.redrawComponents(t,[i._id,l._id]);var w=f?c/p[2]:1,T=h?u/p[3]:1,k=f?p[0]:0,A=h?p[1]:0,M=f?p[0]/p[2]*c:0,S=h?p[1]/p[3]*u:0,E=i._offset-M,L=l._offset-S;n.clipRect.call(o.setTranslate,k,A).call(o.setScale,1/w,1/T),n.plot.call(o.setTranslate,E,L).call(o.setScale,w,T),o.setPointGroupScale(n.zoomScalePts,1/w,1/T),o.setTextPointsScale(n.zoomScaleTxt,1/w,1/T)}s.redrawComponents(t)}},{"../../components/drawing":388,"../../lib":503,"../../registry":638,"./axes":554,"@plotly/d3":58}],582:[function(t,e,r){"use strict";var n=t("../../registry").traceIs,i=t("./axis_autotype");function a(t){return{v:"x",h:"y"}[t.orientation||"v"]}function o(t,e){var r=a(t),i=n(t,"box-violin"),o=n(t._fullInput||{},"candlestick");return i&&!o&&e===r&&void 0===t[r]&&void 0===t[r+"0"]}e.exports=function(t,e,r,s){r("autotypenumbers",s.autotypenumbersDflt),"-"===r("type",(s.splomStash||{}).type)&&(!function(t,e){if("-"!==t.type)return;var r,s=t._id,l=s.charAt(0);-1!==s.indexOf("scene")&&(s=l);var c=function(t,e,r){for(var n=0;n0&&(i["_"+r+"axes"]||{})[e])return i;if((i[r+"axis"]||r)===e){if(o(i,r))return i;if((i[r]||[]).length||i[r+"0"])return i}}}(e,s,l);if(!c)return;if("histogram"===c.type&&l==={v:"y",h:"x"}[c.orientation||"v"])return void(t.type="linear");var u=l+"calendar",f=c[u],h={noMultiCategory:!n(c,"cartesian")||n(c,"noMultiCategory")};"box"===c.type&&c._hasPreCompStats&&l==={h:"x",v:"y"}[c.orientation||"v"]&&(h.noMultiCategory=!0);if(h.autotypenumbers=t.autotypenumbers,o(c,l)){var p=a(c),d=[];for(r=0;r0?".":"")+a;i.isPlainObject(o)?l(o,e,s,n+1):e(s,a,o)}}))}r.manageCommandObserver=function(t,e,n,o){var s={},l=!0;e&&e._commandObserver&&(s=e._commandObserver),s.cache||(s.cache={}),s.lookupTable={};var c=r.hasSimpleAPICommandBindings(t,n,s.lookupTable);if(e&&e._commandObserver){if(c)return s;if(e._commandObserver.remove)return e._commandObserver.remove(),e._commandObserver=null,s}if(c){a(t,c,s.cache),s.check=function(){if(l){var e=a(t,c,s.cache);return e.changed&&o&&void 0!==s.lookupTable[e.value]&&(s.disable(),Promise.resolve(o({value:e.value,type:c.type,prop:c.prop,traces:c.traces,index:s.lookupTable[e.value]})).then(s.enable,s.enable)),e.changed}};for(var u=["plotly_relayout","plotly_redraw","plotly_restyle","plotly_update","plotly_animatingframe","plotly_afterplot"],f=0;f0&&i<0&&(i+=360);var s=(i-n)/4;return{type:"Polygon",coordinates:[[[n,a],[n,o],[n+s,o],[n+2*s,o],[n+3*s,o],[i,o],[i,a],[i-s,a],[i-2*s,a],[i-3*s,a],[n,a]]]}}e.exports=function(t){return new M(t)},S.plot=function(t,e,r){var n=this,i=e[this.id],a=[],o=!1;for(var s in w.layerNameToAdjective)if("frame"!==s&&i["show"+s]){o=!0;break}for(var l=0;l0&&a._module.calcGeoJSON(i,e)}if(!this.updateProjection(t,e)){this.viewInitial&&this.scope===r.scope||this.saveViewInitial(r),this.scope=r.scope,this.updateBaseLayers(e,r),this.updateDims(e,r),this.updateFx(e,r),d.generalUpdatePerTraceModule(this.graphDiv,this,t,r);var o=this.layers.frontplot.select(".scatterlayer");this.dataPoints.point=o.selectAll(".point"),this.dataPoints.text=o.selectAll("text"),this.dataPaths.line=o.selectAll(".js-line");var s=this.layers.backplot.select(".choroplethlayer");this.dataPaths.choropleth=s.selectAll("path"),this.render()}},S.updateProjection=function(t,e){var r=this.graphDiv,n=e[this.id],l=e._size,u=n.domain,f=n.projection,h=n.lonaxis,p=n.lataxis,d=h._ax,m=p._ax,v=this.projection=function(t){var e=t.projection,r=e.type,n=w.projNames[r];n="geo"+c.titleCase(n);for(var l=(i[n]||s[n])(),u=t._isSatellite?180*Math.acos(1/e.distance)/Math.PI:t._isClipped?w.lonaxisSpan[r]/2:null,f=["center","rotate","parallels","clipExtent"],h=function(t){return t?l:[]},p=0;pu*Math.PI/180}return!1},l.getPath=function(){return a().projection(l)},l.getBounds=function(t){return l.getPath().bounds(t)},l.precision(w.precision),t._isSatellite&&l.tilt(e.tilt).distance(e.distance);u&&l.clipAngle(u-w.clipPad);return l}(n),y=[[l.l+l.w*u.x[0],l.t+l.h*(1-u.y[1])],[l.l+l.w*u.x[1],l.t+l.h*(1-u.y[0])]],x=n.center||{},b=f.rotation||{},_=h.range||[],T=p.range||[];if(n.fitbounds){d._length=y[1][0]-y[0][0],m._length=y[1][1]-y[0][1],d.range=g(r,d),m.range=g(r,m);var k=(d.range[0]+d.range[1])/2,A=(m.range[0]+m.range[1])/2;if(n._isScoped)x={lon:k,lat:A};else if(n._isClipped){x={lon:k,lat:A},b={lon:k,lat:A,roll:b.roll};var M=f.type,S=w.lonaxisSpan[M]/2||180,L=w.lataxisSpan[M]/2||90;_=[k-S,k+S],T=[A-L,A+L]}else x={lon:k,lat:A},b={lon:k,lat:b.lat,roll:b.roll}}v.center([x.lon-b.lon,x.lat-b.lat]).rotate([-b.lon,-b.lat,b.roll]).parallels(f.parallels);var C=E(_,T);v.fitExtent(y,C);var P=this.bounds=v.getBounds(C),I=this.fitScale=v.scale(),O=v.translate();if(n.fitbounds){var z=v.getBounds(E(d.range,m.range)),D=Math.min((P[1][0]-P[0][0])/(z[1][0]-z[0][0]),(P[1][1]-P[0][1])/(z[1][1]-z[0][1]));isFinite(D)?v.scale(D*I):c.warn("Something went wrong during"+this.id+"fitbounds computations.")}else v.scale(f.scale*I);var R=this.midPt=[(P[0][0]+P[1][0])/2,(P[0][1]+P[1][1])/2];if(v.translate([O[0]+(R[0]-O[0]),O[1]+(R[1]-O[1])]).clipExtent(P),n._isAlbersUsa){var F=v([x.lon,x.lat]),B=v.translate();v.translate([B[0]-(F[0]-B[0]),B[1]-(F[1]-B[1])])}},S.updateBaseLayers=function(t,e){var r=this,i=r.topojson,a=r.layers,o=r.basePaths;function s(t){return"lonaxis"===t||"lataxis"===t}function l(t){return Boolean(w.lineLayers[t])}function c(t){return Boolean(w.fillLayers[t])}var u=(this.hasChoropleth?w.layersForChoropleth:w.layers).filter((function(t){return l(t)||c(t)?e["show"+t]:!s(t)||e[t].showgrid})),p=r.framework.selectAll(".layer").data(u,String);p.exit().each((function(t){delete a[t],delete o[t],n.select(this).remove()})),p.enter().append("g").attr("class",(function(t){return"layer "+t})).each((function(t){var e=a[t]=n.select(this);"bg"===t?r.bgRect=e.append("rect").style("pointer-events","all"):s(t)?o[t]=e.append("path").style("fill","none"):"backplot"===t?e.append("g").classed("choroplethlayer",!0):"frontplot"===t?e.append("g").classed("scatterlayer",!0):l(t)?o[t]=e.append("path").style("fill","none").style("stroke-miterlimit",2):c(t)&&(o[t]=e.append("path").style("stroke","none"))})),p.order(),p.each((function(r){var n=o[r],a=w.layerNameToAdjective[r];"frame"===r?n.datum(w.sphereSVG):l(r)||c(r)?n.datum(A(i,i.objects[r])):s(r)&&n.datum(function(t,e,r){var n,i,a,o=e[t],s=w.scopeDefaults[e.scope];"lonaxis"===t?(n=s.lonaxisRange,i=s.lataxisRange,a=function(t,e){return[t,e]}):"lataxis"===t&&(n=s.lataxisRange,i=s.lonaxisRange,a=function(t,e){return[e,t]});var l={type:"linear",range:[n[0],n[1]-1e-6],tick0:o.tick0,dtick:o.dtick};m.setConvert(l,r);var c=m.calcTicks(l);e.isScoped||"lonaxis"!==t||c.pop();for(var u=c.length,f=new Array(u),h=0;h-1&&b(n.event,i,[r.xaxis],[r.yaxis],r.id,f),s.indexOf("event")>-1&&p.click(i,n.event))}))}function h(t){return r.projection.invert([t[0]+r.xaxis._offset,t[1]+r.yaxis._offset])}},S.makeFramework=function(){var t=this,e=t.graphDiv,r=e._fullLayout,i="clip"+r._uid+t.id;t.clipDef=r._clips.append("clipPath").attr("id",i),t.clipRect=t.clipDef.append("rect"),t.framework=n.select(t.container).append("g").attr("class","geo "+t.id).call(h.setClipUrl,i,e),t.project=function(e){var r=t.projection(e);return r?[r[0]-t.xaxis._offset,r[1]-t.yaxis._offset]:[null,null]},t.xaxis={_id:"x",c2p:function(e){return t.project(e)[0]}},t.yaxis={_id:"y",c2p:function(e){return t.project(e)[1]}},t.mockAxis={type:"linear",showexponent:"all",exponentformat:"B"},m.setConvert(t.mockAxis,r)},S.saveViewInitial=function(t){var e,r=t.center||{},n=t.projection,i=n.rotation||{};this.viewInitial={fitbounds:t.fitbounds,"projection.scale":n.scale},e=t._isScoped?{"center.lon":r.lon,"center.lat":r.lat}:t._isClipped?{"projection.rotation.lon":i.lon,"projection.rotation.lat":i.lat}:{"center.lon":r.lon,"center.lat":r.lat,"projection.rotation.lon":i.lon},c.extendFlat(this.viewInitial,e)},S.render=function(){var t,e=this.projection,r=e.getPath();function n(t){var r=e(t.lonlat);return r?u(r[0],r[1]):null}function i(t){return e.isLonLatOverEdges(t.lonlat)?"none":null}for(t in this.basePaths)this.basePaths[t].attr("d",r);for(t in this.dataPaths)this.dataPaths[t].attr("d",(function(t){return r(t.geojson)}));for(t in this.dataPoints)this.dataPoints[t].attr("display",i).attr("transform",n)}},{"../../components/color":366,"../../components/dragelement":385,"../../components/drawing":388,"../../components/fx":406,"../../lib":503,"../../lib/geo_location_utils":496,"../../lib/topojson_utils":532,"../../registry":638,"../cartesian/autorange":553,"../cartesian/axes":554,"../cartesian/select":575,"../plots":619,"./constants":587,"./zoom":592,"@plotly/d3":58,"d3-geo":114,"d3-geo-projection":113,"topojson-client":315}],589:[function(t,e,r){"use strict";var n=t("../../plots/get_data").getSubplotCalcData,i=t("../../lib").counterRegex,a=t("./geo"),o="geo",s=i(o),l={};l.geo={valType:"subplotid",dflt:o,editType:"calc"},e.exports={attr:o,name:o,idRoot:o,idRegex:s,attrRegex:s,attributes:l,layoutAttributes:t("./layout_attributes"),supplyLayoutDefaults:t("./layout_defaults"),plot:function(t){for(var e=t._fullLayout,r=t.calcdata,i=e._subplots.geo,s=0;s0&&P<0&&(P+=360);var I,O,z,D=(C+P)/2;if(!p){var R=d?f.projRotate:[D,0,0];I=r("projection.rotation.lon",R[0]),r("projection.rotation.lat",R[1]),r("projection.rotation.roll",R[2]),r("showcoastlines",!d&&x)&&(r("coastlinecolor"),r("coastlinewidth")),r("showocean",!!x&&void 0)&&r("oceancolor")}(p?(O=-96.6,z=38.7):(O=d?D:I,z=(L[0]+L[1])/2),r("center.lon",O),r("center.lat",z),m&&(r("projection.tilt"),r("projection.distance")),g)&&r("projection.parallels",f.projParallels||[0,60]);r("projection.scale"),r("showland",!!x&&void 0)&&r("landcolor"),r("showlakes",!!x&&void 0)&&r("lakecolor"),r("showrivers",!!x&&void 0)&&(r("rivercolor"),r("riverwidth")),r("showcountries",d&&"usa"!==u&&x)&&(r("countrycolor"),r("countrywidth")),("usa"===u||"north america"===u&&50===c)&&(r("showsubunits",x),r("subunitcolor"),r("subunitwidth")),d||r("showframe",x)&&(r("framecolor"),r("framewidth")),r("bgcolor"),r("fitbounds")&&(delete e.projection.scale,d?(delete e.center.lon,delete e.center.lat):v?(delete e.center.lon,delete e.center.lat,delete e.projection.rotation.lon,delete e.projection.rotation.lat,delete e.lonaxis.range,delete e.lataxis.range):(delete e.center.lon,delete e.center.lat,delete e.projection.rotation.lon))}e.exports=function(t,e,r){i(t,e,r,{type:"geo",attributes:s,handleDefaults:c,fullData:r,partition:"y"})}},{"../../lib":503,"../get_data":593,"../subplot_defaults":632,"./constants":587,"./layout_attributes":590}],592:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../lib"),a=t("../../registry"),o=Math.PI/180,s=180/Math.PI,l={cursor:"pointer"},c={cursor:"auto"};function u(t,e){return n.behavior.zoom().translate(e.translate()).scale(e.scale())}function f(t,e,r){var n=t.id,o=t.graphDiv,s=o.layout,l=s[n],c=o._fullLayout,u=c[n],f={},h={};function p(t,e){f[n+"."+t]=i.nestedProperty(l,t).get(),a.call("_storeDirectGUIEdit",s,c._preGUI,f);var r=i.nestedProperty(u,t);r.get()!==e&&(r.set(e),i.nestedProperty(l,t).set(e),h[n+"."+t]=e)}r(p),p("projection.scale",e.scale()/t.fitScale),p("fitbounds",!1),o.emit("plotly_relayout",h)}function h(t,e){var r=u(0,e);function i(r){var n=e.invert(t.midPt);r("center.lon",n[0]),r("center.lat",n[1])}return r.on("zoomstart",(function(){n.select(this).style(l)})).on("zoom",(function(){e.scale(n.event.scale).translate(n.event.translate),t.render();var r=e.invert(t.midPt);t.graphDiv.emit("plotly_relayouting",{"geo.projection.scale":e.scale()/t.fitScale,"geo.center.lon":r[0],"geo.center.lat":r[1]})})).on("zoomend",(function(){n.select(this).style(c),f(t,e,i)})),r}function p(t,e){var r,i,a,o,s,h,p,d,m,g=u(0,e);function v(t){return e.invert(t)}function y(r){var n=e.rotate(),i=e.invert(t.midPt);r("projection.rotation.lon",-n[0]),r("center.lon",i[0]),r("center.lat",i[1])}return g.on("zoomstart",(function(){n.select(this).style(l),r=n.mouse(this),i=e.rotate(),a=e.translate(),o=i,s=v(r)})).on("zoom",(function(){if(h=n.mouse(this),function(t){var r=v(t);if(!r)return!0;var n=e(r);return Math.abs(n[0]-t[0])>2||Math.abs(n[1]-t[1])>2}(r))return g.scale(e.scale()),void g.translate(e.translate());e.scale(n.event.scale),e.translate([a[0],n.event.translate[1]]),s?v(h)&&(d=v(h),p=[o[0]+(d[0]-s[0]),i[1],i[2]],e.rotate(p),o=p):s=v(r=h),m=!0,t.render();var l=e.rotate(),c=e.invert(t.midPt);t.graphDiv.emit("plotly_relayouting",{"geo.projection.scale":e.scale()/t.fitScale,"geo.center.lon":c[0],"geo.center.lat":c[1],"geo.projection.rotation.lon":-l[0]})})).on("zoomend",(function(){n.select(this).style(c),m&&f(t,e,y)})),g}function d(t,e){var r,i={r:e.rotate(),k:e.scale()},a=u(0,e),o=function(t){var e=0,r=arguments.length,i=[];for(;++ed?(a=(f>0?90:-90)-p,i=0):(a=Math.asin(f/d)*s-p,i=Math.sqrt(d*d-f*f));var m=180-a-2*p,g=(Math.atan2(h,u)-Math.atan2(c,i))*s,v=(Math.atan2(h,u)-Math.atan2(c,-i))*s;return b(r[0],r[1],a,g)<=b(r[0],r[1],m,v)?[a,g,r[2]]:[m,v,r[2]]}function b(t,e,r,n){var i=_(r-t),a=_(n-e);return Math.sqrt(i*i+a*a)}function _(t){return(t%360+540)%360-180}function w(t,e,r){var n=r*o,i=t.slice(),a=0===e?1:0,s=2===e?1:2,l=Math.cos(n),c=Math.sin(n);return i[a]=t[a]*l-t[s]*c,i[s]=t[s]*l+t[a]*c,i}function T(t){return[Math.atan2(2*(t[0]*t[1]+t[2]*t[3]),1-2*(t[1]*t[1]+t[2]*t[2]))*s,Math.asin(Math.max(-1,Math.min(1,2*(t[0]*t[2]-t[3]*t[1]))))*s,Math.atan2(2*(t[0]*t[3]+t[1]*t[2]),1-2*(t[2]*t[2]+t[3]*t[3]))*s]}function k(t,e){for(var r=0,n=0,i=t.length;nMath.abs(s)?(c.boxEnd[1]=c.boxStart[1]+Math.abs(a)*_*(s>=0?1:-1),c.boxEnd[1]l[3]&&(c.boxEnd[1]=l[3],c.boxEnd[0]=c.boxStart[0]+(l[3]-c.boxStart[1])/Math.abs(_))):(c.boxEnd[0]=c.boxStart[0]+Math.abs(s)/_*(a>=0?1:-1),c.boxEnd[0]l[2]&&(c.boxEnd[0]=l[2],c.boxEnd[1]=c.boxStart[1]+(l[2]-c.boxStart[0])*Math.abs(_)))}}else c.boxEnabled?(a=c.boxStart[0]!==c.boxEnd[0],s=c.boxStart[1]!==c.boxEnd[1],a||s?(a&&(g(0,c.boxStart[0],c.boxEnd[0]),t.xaxis.autorange=!1),s&&(g(1,c.boxStart[1],c.boxEnd[1]),t.yaxis.autorange=!1),t.relayoutCallback()):t.glplot.setDirty(),c.boxEnabled=!1,c.boxInited=!1):c.boxInited&&(c.boxInited=!1);break;case"pan":c.boxEnabled=!1,c.boxInited=!1,e?(c.panning||(c.dragStart[0]=n,c.dragStart[1]=i),Math.abs(c.dragStart[0]-n).999&&(m="turntable"):m="turntable")}else m="turntable";r("dragmode",m),r("hovermode",n.getDfltFromLayout("hovermode"))}e.exports=function(t,e,r){var i=e._basePlotModules.length>1;o(t,e,r,{type:"gl3d",attributes:l,handleDefaults:u,fullLayout:e,font:e.font,fullData:r,getDfltFromLayout:function(e){if(!i)return n.validate(t[e],l[e])?t[e]:void 0},autotypenumbersDflt:e.autotypenumbers,paper_bgcolor:e.paper_bgcolor,calendar:e.calendar})}},{"../../../components/color":366,"../../../lib":503,"../../../registry":638,"../../get_data":593,"../../subplot_defaults":632,"./axis_defaults":601,"./layout_attributes":604}],604:[function(t,e,r){"use strict";var n=t("./axis_attributes"),i=t("../../domain").attributes,a=t("../../../lib/extend").extendFlat,o=t("../../../lib").counterRegex;function s(t,e,r){return{x:{valType:"number",dflt:t,editType:"camera"},y:{valType:"number",dflt:e,editType:"camera"},z:{valType:"number",dflt:r,editType:"camera"},editType:"camera"}}e.exports={_arrayAttrRegexps:[o("scene",".annotations",!0)],bgcolor:{valType:"color",dflt:"rgba(0,0,0,0)",editType:"plot"},camera:{up:a(s(0,0,1),{}),center:a(s(0,0,0),{}),eye:a(s(1.25,1.25,1.25),{}),projection:{type:{valType:"enumerated",values:["perspective","orthographic"],dflt:"perspective",editType:"calc"},editType:"calc"},editType:"camera"},domain:i({name:"scene",editType:"plot"}),aspectmode:{valType:"enumerated",values:["auto","cube","data","manual"],dflt:"auto",editType:"plot",impliedEdits:{"aspectratio.x":void 0,"aspectratio.y":void 0,"aspectratio.z":void 0}},aspectratio:{x:{valType:"number",min:0,editType:"plot",impliedEdits:{"^aspectmode":"manual"}},y:{valType:"number",min:0,editType:"plot",impliedEdits:{"^aspectmode":"manual"}},z:{valType:"number",min:0,editType:"plot",impliedEdits:{"^aspectmode":"manual"}},editType:"plot",impliedEdits:{aspectmode:"manual"}},xaxis:n,yaxis:n,zaxis:n,dragmode:{valType:"enumerated",values:["orbit","turntable","zoom","pan",!1],editType:"plot"},hovermode:{valType:"enumerated",values:["closest",!1],dflt:"closest",editType:"modebar"},uirevision:{valType:"any",editType:"none"},editType:"plot",_deprecated:{cameraposition:{valType:"info_array",editType:"camera"}}}},{"../../../lib":503,"../../../lib/extend":493,"../../domain":584,"./axis_attributes":600}],605:[function(t,e,r){"use strict";var n=t("../../../lib/str2rgbarray"),i=["xaxis","yaxis","zaxis"];function a(){this.enabled=[!0,!0,!0],this.colors=[[0,0,0,1],[0,0,0,1],[0,0,0,1]],this.drawSides=[!0,!0,!0],this.lineWidth=[1,1,1]}a.prototype.merge=function(t){for(var e=0;e<3;++e){var r=t[i[e]];r.visible?(this.enabled[e]=r.showspikes,this.colors[e]=n(r.spikecolor),this.drawSides[e]=r.spikesides,this.lineWidth[e]=r.spikethickness):(this.enabled[e]=!1,this.drawSides[e]=!1)}},e.exports=function(t){var e=new a;return e.merge(t),e}},{"../../../lib/str2rgbarray":528}],606:[function(t,e,r){"use strict";e.exports=function(t){for(var e=t.axesOptions,r=t.glplot.axesPixels,s=t.fullSceneLayout,l=[[],[],[]],c=0;c<3;++c){var u=s[a[c]];if(u._length=(r[c].hi-r[c].lo)*r[c].pixelsPerDataUnit/t.dataScale[c],Math.abs(u._length)===1/0||isNaN(u._length))l[c]=[];else{u._input_range=u.range.slice(),u.range[0]=r[c].lo/t.dataScale[c],u.range[1]=r[c].hi/t.dataScale[c],u._m=1/(t.dataScale[c]*r[c].pixelsPerDataUnit),u.range[0]===u.range[1]&&(u.range[0]-=1,u.range[1]+=1);var f=u.tickmode;if("auto"===u.tickmode){u.tickmode="linear";var h=u.nticks||i.constrain(u._length/40,4,9);n.autoTicks(u,Math.abs(u.range[1]-u.range[0])/h)}for(var p=n.calcTicks(u,{msUTC:!0}),d=0;d/g," "));l[c]=p,u.tickmode=f}}e.ticks=l;for(c=0;c<3;++c){o[c]=.5*(t.glplot.bounds[0][c]+t.glplot.bounds[1][c]);for(d=0;d<2;++d)e.bounds[d][c]=t.glplot.bounds[d][c]}t.contourLevels=function(t){for(var e=new Array(3),r=0;r<3;++r){for(var n=t[r],i=new Array(n.length),a=0;ar.deltaY?1.1:1/1.1,a=t.glplot.getAspectratio();t.glplot.setAspectratio({x:n*a.x,y:n*a.y,z:n*a.z})}i(t)}}),!!c&&{passive:!1}),t.glplot.canvas.addEventListener("mousemove",(function(){if(!1!==t.fullSceneLayout.dragmode&&0!==t.camera.mouseListener.buttons){var e=n();t.graphDiv.emit("plotly_relayouting",e)}})),t.staticMode||t.glplot.canvas.addEventListener("webglcontextlost",(function(r){e&&e.emit&&e.emit("plotly_webglcontextlost",{event:r,layer:t.id})}),!1)),t.glplot.oncontextloss=function(){t.recoverContext()},t.glplot.onrender=function(){t.render()},!0},w.render=function(){var t,e=this,r=e.graphDiv,n=e.svgContainer,i=e.container.getBoundingClientRect();r._fullLayout._calcInverseTransform(r);var a=r._fullLayout._invScaleX,o=r._fullLayout._invScaleY,s=i.width*a,l=i.height*o;n.setAttributeNS(null,"viewBox","0 0 "+s+" "+l),n.setAttributeNS(null,"width",s),n.setAttributeNS(null,"height",l),b(e),e.glplot.axes.update(e.axesOptions);for(var c=Object.keys(e.traces),u=null,h=e.glplot.selection,m=0;m")):"isosurface"===t.type||"volume"===t.type?(T.valueLabel=p.hoverLabelText(e._mockAxis,e._mockAxis.d2l(h.traceCoordinate[3]),t.valuehoverformat),S.push("value: "+T.valueLabel),h.textLabel&&S.push(h.textLabel),x=S.join("
")):x=h.textLabel;var E={x:h.traceCoordinate[0],y:h.traceCoordinate[1],z:h.traceCoordinate[2],data:_._input,fullData:_,curveNumber:_.index,pointNumber:w};d.appendArrayPointValue(E,_,w),t._module.eventData&&(E=_._module.eventData(E,h,_,{},w));var L={points:[E]};if(e.fullSceneLayout.hovermode){var C=[];d.loneHover({trace:_,x:(.5+.5*y[0]/y[3])*s,y:(.5-.5*y[1]/y[3])*l,xLabel:T.xLabel,yLabel:T.yLabel,zLabel:T.zLabel,text:x,name:u.name,color:d.castHoverOption(_,w,"bgcolor")||u.color,borderColor:d.castHoverOption(_,w,"bordercolor"),fontFamily:d.castHoverOption(_,w,"font.family"),fontSize:d.castHoverOption(_,w,"font.size"),fontColor:d.castHoverOption(_,w,"font.color"),nameLength:d.castHoverOption(_,w,"namelength"),textAlign:d.castHoverOption(_,w,"align"),hovertemplate:f.castOption(_,w,"hovertemplate"),hovertemplateLabels:f.extendFlat({},E,T),eventData:[E]},{container:n,gd:r,inOut_bbox:C}),E.bbox=C[0]}h.buttons&&h.distance<5?r.emit("plotly_click",L):r.emit("plotly_hover",L),this.oldEventData=L}else d.loneUnhover(n),this.oldEventData&&r.emit("plotly_unhover",this.oldEventData),this.oldEventData=void 0;e.drawAnnotations(e)},w.recoverContext=function(){var t=this;t.glplot.dispose();var e=function(){t.glplot.gl.isContextLost()?requestAnimationFrame(e):t.initializeGLPlot()?t.plot.apply(t,t.plotArgs):f.error("Catastrophic and unrecoverable WebGL error. Context lost.")};requestAnimationFrame(e)};var k=["xaxis","yaxis","zaxis"];function A(t,e,r){for(var n=t.fullSceneLayout,i=0;i<3;i++){var a=k[i],o=a.charAt(0),s=n[a],l=e[o],c=e[o+"calendar"],u=e["_"+o+"length"];if(f.isArrayOrTypedArray(l))for(var h,p=0;p<(u||l.length);p++)if(f.isArrayOrTypedArray(l[p]))for(var d=0;dg[1][a])g[0][a]=-1,g[1][a]=1;else{var L=g[1][a]-g[0][a];g[0][a]-=L/32,g[1][a]+=L/32}if("reversed"===s.autorange){var C=g[0][a];g[0][a]=g[1][a],g[1][a]=C}}else{var P=s.range;g[0][a]=s.r2l(P[0]),g[1][a]=s.r2l(P[1])}g[0][a]===g[1][a]&&(g[0][a]-=1,g[1][a]+=1),v[a]=g[1][a]-g[0][a],this.glplot.setBounds(a,{min:g[0][a]*h[a],max:g[1][a]*h[a]})}var I=c.aspectmode;if("cube"===I)d=[1,1,1];else if("manual"===I){var O=c.aspectratio;d=[O.x,O.y,O.z]}else{if("auto"!==I&&"data"!==I)throw new Error("scene.js aspectRatio was not one of the enumerated types");var z=[1,1,1];for(a=0;a<3;++a){var D=y[l=(s=c[k[a]]).type];z[a]=Math.pow(D.acc,1/D.count)/h[a]}d="data"===I||Math.max.apply(null,z)/Math.min.apply(null,z)<=4?z:[1,1,1]}c.aspectratio.x=u.aspectratio.x=d[0],c.aspectratio.y=u.aspectratio.y=d[1],c.aspectratio.z=u.aspectratio.z=d[2],this.glplot.setAspectratio(c.aspectratio),this.viewInitial.aspectratio||(this.viewInitial.aspectratio={x:c.aspectratio.x,y:c.aspectratio.y,z:c.aspectratio.z}),this.viewInitial.aspectmode||(this.viewInitial.aspectmode=c.aspectmode);var R=c.domain||null,F=e._size||null;if(R&&F){var B=this.container.style;B.position="absolute",B.left=F.l+R.x[0]*F.w+"px",B.top=F.t+(1-R.y[1])*F.h+"px",B.width=F.w*(R.x[1]-R.x[0])+"px",B.height=F.h*(R.y[1]-R.y[0])+"px"}this.glplot.redraw()}},w.destroy=function(){this.glplot&&(this.camera.mouseListener.enabled=!1,this.container.removeEventListener("wheel",this.camera.wheelListener),this.camera=null,this.glplot.dispose(),this.container.parentNode.removeChild(this.container),this.glplot=null)},w.getCamera=function(){var t;return this.camera.view.recalcMatrix(this.camera.view.lastT()),{up:{x:(t=this.camera).up[0],y:t.up[1],z:t.up[2]},center:{x:t.center[0],y:t.center[1],z:t.center[2]},eye:{x:t.eye[0],y:t.eye[1],z:t.eye[2]},projection:{type:!0===t._ortho?"orthographic":"perspective"}}},w.setViewport=function(t){var e,r=t.camera;this.camera.lookAt.apply(this,[[(e=r).eye.x,e.eye.y,e.eye.z],[e.center.x,e.center.y,e.center.z],[e.up.x,e.up.y,e.up.z]]),this.glplot.setAspectratio(t.aspectratio),"orthographic"===r.projection.type!==this.camera._ortho&&(this.glplot.redraw(),this.glplot.clearRGBA(),this.glplot.dispose(),this.initializeGLPlot())},w.isCameraChanged=function(t){var e=this.getCamera(),r=f.nestedProperty(t,this.id+".camera").get();function n(t,e,r,n){var i=["up","center","eye"],a=["x","y","z"];return e[i[r]]&&t[i[r]][a[n]]===e[i[r]][a[n]]}var i=!1;if(void 0===r)i=!0;else{for(var a=0;a<3;a++)for(var o=0;o<3;o++)if(!n(e,r,a,o)){i=!0;break}(!r.projection||e.projection&&e.projection.type!==r.projection.type)&&(i=!0)}return i},w.isAspectChanged=function(t){var e=this.glplot.getAspectratio(),r=f.nestedProperty(t,this.id+".aspectratio").get();return void 0===r||r.x!==e.x||r.y!==e.y||r.z!==e.z},w.saveLayout=function(t){var e,r,n,i,a,o,s=this.fullLayout,l=this.isCameraChanged(t),c=this.isAspectChanged(t),h=l||c;if(h){var p={};if(l&&(e=this.getCamera(),n=(r=f.nestedProperty(t,this.id+".camera")).get(),p[this.id+".camera"]=n),c&&(i=this.glplot.getAspectratio(),o=(a=f.nestedProperty(t,this.id+".aspectratio")).get(),p[this.id+".aspectratio"]=o),u.call("_storeDirectGUIEdit",t,s._preGUI,p),l)r.set(e),f.nestedProperty(s,this.id+".camera").set(e);if(c)a.set(i),f.nestedProperty(s,this.id+".aspectratio").set(i),this.glplot.redraw()}return h},w.updateFx=function(t,e){var r=this.camera;if(r)if("orbit"===t)r.mode="orbit",r.keyBindingMode="rotate";else if("turntable"===t){r.up=[0,0,1],r.mode="turntable",r.keyBindingMode="rotate";var n=this.graphDiv,i=n._fullLayout,a=this.fullSceneLayout.camera,o=a.up.x,s=a.up.y,l=a.up.z;if(l/Math.sqrt(o*o+s*s+l*l)<.999){var c=this.id+".camera.up",h={x:0,y:0,z:1},p={};p[c]=h;var d=n.layout;u.call("_storeDirectGUIEdit",d,i._preGUI,p),a.up=h,f.nestedProperty(d,c).set(h)}}else r.keyBindingMode=t;this.fullSceneLayout.hovermode=e},w.toImage=function(t){t||(t="png"),this.staticMode&&this.container.appendChild(n),this.glplot.redraw();var e=this.glplot.gl,r=e.drawingBufferWidth,i=e.drawingBufferHeight;e.bindFramebuffer(e.FRAMEBUFFER,null);var a=new Uint8Array(r*i*4);e.readPixels(0,0,r,i,e.RGBA,e.UNSIGNED_BYTE,a),function(t,e,r){for(var n=0,i=r-1;n0)for(var s=255/o,l=0;l<3;++l)t[a+l]=Math.min(s*t[a+l],255)}}(a,r,i);var o=document.createElement("canvas");o.width=r,o.height=i;var s,l=o.getContext("2d",{willReadFrequently:!0}),c=l.createImageData(r,i);switch(c.data.set(a),l.putImageData(c,0,0),t){case"jpeg":s=o.toDataURL("image/jpeg");break;case"webp":s=o.toDataURL("image/webp");break;default:s=o.toDataURL("image/png")}return this.staticMode&&this.container.removeChild(n),s},w.setConvert=function(){for(var t=0;t<3;t++){var e=this.fullSceneLayout[k[t]];p.setConvert(e,this.fullLayout),e.setScale=f.noop}},w.make4thDimension=function(){var t=this.graphDiv._fullLayout;this._mockAxis={type:"linear",showexponent:"all",exponentformat:"B"},p.setConvert(this._mockAxis,t)},e.exports=_},{"../../../stackgl_modules":1124,"../../components/fx":406,"../../lib":503,"../../lib/show_no_webgl_msg":525,"../../lib/str2rgbarray":528,"../../plots/cartesian/axes":554,"../../registry":638,"./layout/convert":602,"./layout/spikes":605,"./layout/tick_marks":606,"./project":607,"has-passive-events":229,"webgl-context":331}],609:[function(t,e,r){"use strict";e.exports=function(t,e,r,n){n=n||t.length;for(var i=new Array(n),a=0;aOpenStreetMap contributors',a=['\xa9 Carto',i].join(" "),o=['Map tiles by Stamen Design','under CC BY 3.0',"|",'Data by OpenStreetMap contributors','under ODbL'].join(" "),s={"open-street-map":{id:"osm",version:8,sources:{"plotly-osm-tiles":{type:"raster",attribution:i,tiles:["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png","https://b.tile.openstreetmap.org/{z}/{x}/{y}.png"],tileSize:256}},layers:[{id:"plotly-osm-tiles",type:"raster",source:"plotly-osm-tiles",minzoom:0,maxzoom:22}]},"white-bg":{id:"white-bg",version:8,sources:{},layers:[{id:"white-bg",type:"background",paint:{"background-color":"#FFFFFF"},minzoom:0,maxzoom:22}]},"carto-positron":{id:"carto-positron",version:8,sources:{"plotly-carto-positron":{type:"raster",attribution:a,tiles:["https://cartodb-basemaps-c.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png"],tileSize:256}},layers:[{id:"plotly-carto-positron",type:"raster",source:"plotly-carto-positron",minzoom:0,maxzoom:22}]},"carto-darkmatter":{id:"carto-darkmatter",version:8,sources:{"plotly-carto-darkmatter":{type:"raster",attribution:a,tiles:["https://cartodb-basemaps-c.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png"],tileSize:256}},layers:[{id:"plotly-carto-darkmatter",type:"raster",source:"plotly-carto-darkmatter",minzoom:0,maxzoom:22}]},"stamen-terrain":{id:"stamen-terrain",version:8,sources:{"plotly-stamen-terrain":{type:"raster",attribution:o,tiles:["https://stamen-tiles.a.ssl.fastly.net/terrain/{z}/{x}/{y}.png"],tileSize:256}},layers:[{id:"plotly-stamen-terrain",type:"raster",source:"plotly-stamen-terrain",minzoom:0,maxzoom:22}]},"stamen-toner":{id:"stamen-toner",version:8,sources:{"plotly-stamen-toner":{type:"raster",attribution:o,tiles:["https://stamen-tiles.a.ssl.fastly.net/toner/{z}/{x}/{y}.png"],tileSize:256}},layers:[{id:"plotly-stamen-toner",type:"raster",source:"plotly-stamen-toner",minzoom:0,maxzoom:22}]},"stamen-watercolor":{id:"stamen-watercolor",version:8,sources:{"plotly-stamen-watercolor":{type:"raster",attribution:['Map tiles by Stamen Design','under CC BY 3.0',"|",'Data by OpenStreetMap contributors','under CC BY SA'].join(" "),tiles:["https://stamen-tiles.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.png"],tileSize:256}},layers:[{id:"plotly-stamen-watercolor",type:"raster",source:"plotly-stamen-watercolor",minzoom:0,maxzoom:22}]}},l=n(s);e.exports={requiredVersion:"1.10.1",styleUrlPrefix:"mapbox://styles/mapbox/",styleUrlSuffix:"v9",styleValuesMapbox:["basic","streets","outdoors","light","dark","satellite","satellite-streets"],styleValueDflt:"basic",stylesNonMapbox:s,styleValuesNonMapbox:l,traceLayerPrefix:"plotly-trace-layer-",layoutLayerPrefix:"plotly-layout-layer-",wrongVersionErrorMsg:["Your custom plotly.js bundle is not using the correct mapbox-gl version","Please install mapbox-gl@1.10.1."].join("\n"),noAccessTokenErrorMsg:["Missing Mapbox access token.","Mapbox trace type require a Mapbox access token to be registered.","For example:"," Plotly.newPlot(gd, data, layout, { mapboxAccessToken: 'my-access-token' });","More info here: https://www.mapbox.com/help/define-access-token/"].join("\n"),missingStyleErrorMsg:["No valid mapbox style found, please set `mapbox.style` to one of:",l.join(", "),"or register a Mapbox access token to use a Mapbox-served style."].join("\n"),multipleTokensErrorMsg:["Set multiple mapbox access token across different mapbox subplot,","using first token found as mapbox-gl does not allow multipleaccess tokens on the same page."].join("\n"),mapOnErrorMsg:"Mapbox error.",mapboxLogo:{path0:"m 10.5,1.24 c -5.11,0 -9.25,4.15 -9.25,9.25 0,5.1 4.15,9.25 9.25,9.25 5.1,0 9.25,-4.15 9.25,-9.25 0,-5.11 -4.14,-9.25 -9.25,-9.25 z m 4.39,11.53 c -1.93,1.93 -4.78,2.31 -6.7,2.31 -0.7,0 -1.41,-0.05 -2.1,-0.16 0,0 -1.02,-5.64 2.14,-8.81 0.83,-0.83 1.95,-1.28 3.13,-1.28 1.27,0 2.49,0.51 3.39,1.42 1.84,1.84 1.89,4.75 0.14,6.52 z",path1:"M 10.5,-0.01 C 4.7,-0.01 0,4.7 0,10.49 c 0,5.79 4.7,10.5 10.5,10.5 5.8,0 10.5,-4.7 10.5,-10.5 C 20.99,4.7 16.3,-0.01 10.5,-0.01 Z m 0,19.75 c -5.11,0 -9.25,-4.15 -9.25,-9.25 0,-5.1 4.14,-9.26 9.25,-9.26 5.11,0 9.25,4.15 9.25,9.25 0,5.13 -4.14,9.26 -9.25,9.26 z",path2:"M 14.74,6.25 C 12.9,4.41 9.98,4.35 8.23,6.1 5.07,9.27 6.09,14.91 6.09,14.91 c 0,0 5.64,1.02 8.81,-2.14 C 16.64,11 16.59,8.09 14.74,6.25 Z m -2.27,4.09 -0.91,1.87 -0.9,-1.87 -1.86,-0.91 1.86,-0.9 0.9,-1.87 0.91,1.87 1.86,0.9 z",polygon:"11.56,12.21 10.66,10.34 8.8,9.43 10.66,8.53 11.56,6.66 12.47,8.53 14.33,9.43 12.47,10.34"},styleRules:{map:"overflow:hidden;position:relative;","missing-css":"display:none;",canary:"background-color:salmon;","ctrl-bottom-left":"position: absolute; pointer-events: none; z-index: 2; bottom: 0; left: 0;","ctrl-bottom-right":"position: absolute; pointer-events: none; z-index: 2; right: 0; bottom: 0;",ctrl:"clear: both; pointer-events: auto; transform: translate(0, 0);","ctrl-attrib.mapboxgl-compact .mapboxgl-ctrl-attrib-inner":"display: none;","ctrl-attrib.mapboxgl-compact:hover .mapboxgl-ctrl-attrib-inner":"display: block; margin-top:2px","ctrl-attrib.mapboxgl-compact:hover":"padding: 2px 24px 2px 4px; visibility: visible; margin-top: 6px;","ctrl-attrib.mapboxgl-compact::after":'content: ""; cursor: pointer; position: absolute; background-image: url(\'data:image/svg+xml;charset=utf-8,%3Csvg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"%3E %3Cpath fill="%23333333" fill-rule="evenodd" d="M4,10a6,6 0 1,0 12,0a6,6 0 1,0 -12,0 M9,7a1,1 0 1,0 2,0a1,1 0 1,0 -2,0 M9,10a1,1 0 1,1 2,0l0,3a1,1 0 1,1 -2,0"/%3E %3C/svg%3E\'); background-color: rgba(255, 255, 255, 0.5); width: 24px; height: 24px; box-sizing: border-box; border-radius: 12px;',"ctrl-attrib.mapboxgl-compact":"min-height: 20px; padding: 0; margin: 10px; position: relative; background-color: #fff; border-radius: 3px 12px 12px 3px;","ctrl-bottom-right > .mapboxgl-ctrl-attrib.mapboxgl-compact::after":"bottom: 0; right: 0","ctrl-bottom-left > .mapboxgl-ctrl-attrib.mapboxgl-compact::after":"bottom: 0; left: 0","ctrl-bottom-left .mapboxgl-ctrl":"margin: 0 0 10px 10px; float: left;","ctrl-bottom-right .mapboxgl-ctrl":"margin: 0 10px 10px 0; float: right;","ctrl-attrib":"color: rgba(0, 0, 0, 0.75); text-decoration: none; font-size: 12px","ctrl-attrib a":"color: rgba(0, 0, 0, 0.75); text-decoration: none; font-size: 12px","ctrl-attrib a:hover":"color: inherit; text-decoration: underline;","ctrl-attrib .mapbox-improve-map":"font-weight: bold; margin-left: 2px;","attrib-empty":"display: none;","ctrl-logo":'display:block; width: 21px; height: 21px; background-image: url(\'data:image/svg+xml;charset=utf-8,%3C?xml version="1.0" encoding="utf-8"?%3E %3Csvg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 21 21" style="enable-background:new 0 0 21 21;" xml:space="preserve"%3E%3Cg transform="translate(0,0.01)"%3E%3Cpath d="m 10.5,1.24 c -5.11,0 -9.25,4.15 -9.25,9.25 0,5.1 4.15,9.25 9.25,9.25 5.1,0 9.25,-4.15 9.25,-9.25 0,-5.11 -4.14,-9.25 -9.25,-9.25 z m 4.39,11.53 c -1.93,1.93 -4.78,2.31 -6.7,2.31 -0.7,0 -1.41,-0.05 -2.1,-0.16 0,0 -1.02,-5.64 2.14,-8.81 0.83,-0.83 1.95,-1.28 3.13,-1.28 1.27,0 2.49,0.51 3.39,1.42 1.84,1.84 1.89,4.75 0.14,6.52 z" style="opacity:0.9;fill:%23ffffff;enable-background:new" class="st0"/%3E%3Cpath d="M 10.5,-0.01 C 4.7,-0.01 0,4.7 0,10.49 c 0,5.79 4.7,10.5 10.5,10.5 5.8,0 10.5,-4.7 10.5,-10.5 C 20.99,4.7 16.3,-0.01 10.5,-0.01 Z m 0,19.75 c -5.11,0 -9.25,-4.15 -9.25,-9.25 0,-5.1 4.14,-9.26 9.25,-9.26 5.11,0 9.25,4.15 9.25,9.25 0,5.13 -4.14,9.26 -9.25,9.26 z" style="opacity:0.35;enable-background:new" class="st1"/%3E%3Cpath d="M 14.74,6.25 C 12.9,4.41 9.98,4.35 8.23,6.1 5.07,9.27 6.09,14.91 6.09,14.91 c 0,0 5.64,1.02 8.81,-2.14 C 16.64,11 16.59,8.09 14.74,6.25 Z m -2.27,4.09 -0.91,1.87 -0.9,-1.87 -1.86,-0.91 1.86,-0.9 0.9,-1.87 0.91,1.87 1.86,0.9 z" style="opacity:0.35;enable-background:new" class="st1"/%3E%3Cpolygon points="11.56,12.21 10.66,10.34 8.8,9.43 10.66,8.53 11.56,6.66 12.47,8.53 14.33,9.43 12.47,10.34 " style="opacity:0.9;fill:%23ffffff;enable-background:new" class="st0"/%3E%3C/g%3E%3C/svg%3E\')'}}},{"../../lib/sort_object_keys":526}],612:[function(t,e,r){"use strict";var n=t("../../lib");e.exports=function(t,e){var r=t.split(" "),i=r[0],a=r[1],o=n.isArrayOrTypedArray(e)?n.mean(e):e,s=.5+o/100,l=1.5+o/100,c=["",""],u=[0,0];switch(i){case"top":c[0]="top",u[1]=-l;break;case"bottom":c[0]="bottom",u[1]=l}switch(a){case"left":c[1]="right",u[0]=-s;break;case"right":c[1]="left",u[0]=s}return{anchor:c[0]&&c[1]?c.join("-"):c[0]?c[0]:c[1]?c[1]:"center",offset:u}}},{"../../lib":503}],613:[function(t,e,r){"use strict";var n=t("mapbox-gl/dist/mapbox-gl-unminified"),i=t("../../lib"),a=i.strTranslate,o=i.strScale,s=t("../../plots/get_data").getSubplotCalcData,l=t("../../constants/xmlns_namespaces"),c=t("@plotly/d3"),u=t("../../components/drawing"),f=t("../../lib/svg_text_utils"),h=t("./mapbox"),p=r.constants=t("./constants");function d(t){return"string"==typeof t&&(-1!==p.styleValuesMapbox.indexOf(t)||0===t.indexOf("mapbox://"))}r.name="mapbox",r.attr="subplot",r.idRoot="mapbox",r.idRegex=r.attrRegex=i.counterRegex("mapbox"),r.attributes={subplot:{valType:"subplotid",dflt:"mapbox",editType:"calc"}},r.layoutAttributes=t("./layout_attributes"),r.supplyLayoutDefaults=t("./layout_defaults"),r.plot=function(t){var e=t._fullLayout,r=t.calcdata,a=e._subplots.mapbox;if(n.version!==p.requiredVersion)throw new Error(p.wrongVersionErrorMsg);var o=function(t,e){var r=t._fullLayout;if(""===t._context.mapboxAccessToken)return"";for(var n=[],a=[],o=!1,s=!1,l=0;l1&&i.warn(p.multipleTokensErrorMsg),n[0]):(a.length&&i.log(["Listed mapbox access token(s)",a.join(","),"but did not use a Mapbox map style, ignoring token(s)."].join(" ")),"")}(t,a);n.accessToken=o;for(var l=0;l_/2){var w=v.split("|").join("
");x.text(w).attr("data-unformatted",w).call(f.convertToTspans,t),b=u.bBox(x.node())}x.attr("transform",a(-3,8-b.height)),y.insert("rect",".static-attribution").attr({x:-b.width-6,y:-b.height-3,width:b.width+6,height:b.height+3,fill:"rgba(255, 255, 255, 0.75)"});var T=1;b.width+6>_&&(T=_/(b.width+6));var k=[n.l+n.w*h.x[1],n.t+n.h*(1-h.y[0])];y.attr("transform",a(k[0],k[1])+o(T))}},r.updateFx=function(t){for(var e=t._fullLayout,r=e._subplots.mapbox,n=0;n0){for(var r=0;r0}function u(t){var e={},r={};switch(t.type){case"circle":n.extendFlat(r,{"circle-radius":t.circle.radius,"circle-color":t.color,"circle-opacity":t.opacity});break;case"line":n.extendFlat(r,{"line-width":t.line.width,"line-color":t.color,"line-opacity":t.opacity,"line-dasharray":t.line.dash});break;case"fill":n.extendFlat(r,{"fill-color":t.color,"fill-outline-color":t.fill.outlinecolor,"fill-opacity":t.opacity});break;case"symbol":var i=t.symbol,o=a(i.textposition,i.iconsize);n.extendFlat(e,{"icon-image":i.icon+"-15","icon-size":i.iconsize/10,"text-field":i.text,"text-size":i.textfont.size,"text-anchor":o.anchor,"text-offset":o.offset,"symbol-placement":i.placement}),n.extendFlat(r,{"icon-color":t.color,"text-color":i.textfont.color,"text-opacity":t.opacity});break;case"raster":n.extendFlat(r,{"raster-fade-duration":0,"raster-opacity":t.opacity})}return{layout:e,paint:r}}l.update=function(t){this.visible?this.needsNewImage(t)?this.updateImage(t):this.needsNewSource(t)?(this.removeLayer(),this.updateSource(t),this.updateLayer(t)):this.needsNewLayer(t)?this.updateLayer(t):this.updateStyle(t):(this.updateSource(t),this.updateLayer(t)),this.visible=c(t)},l.needsNewImage=function(t){return this.subplot.map.getSource(this.idSource)&&"image"===this.sourceType&&"image"===t.sourcetype&&(this.source!==t.source||JSON.stringify(this.coordinates)!==JSON.stringify(t.coordinates))},l.needsNewSource=function(t){return this.sourceType!==t.sourcetype||JSON.stringify(this.source)!==JSON.stringify(t.source)||this.layerType!==t.type},l.needsNewLayer=function(t){return this.layerType!==t.type||this.below!==this.subplot.belowLookup["layout-"+this.index]},l.lookupBelow=function(){return this.subplot.belowLookup["layout-"+this.index]},l.updateImage=function(t){this.subplot.map.getSource(this.idSource).updateImage({url:t.source,coordinates:t.coordinates});var e=this.findFollowingMapboxLayerId(this.lookupBelow());null!==e&&this.subplot.map.moveLayer(this.idLayer,e)},l.updateSource=function(t){var e=this.subplot.map;if(e.getSource(this.idSource)&&e.removeSource(this.idSource),this.sourceType=t.sourcetype,this.source=t.source,c(t)){var r=function(t){var e,r=t.sourcetype,n=t.source,a={type:r};"geojson"===r?e="data":"vector"===r?e="string"==typeof n?"url":"tiles":"raster"===r?(e="tiles",a.tileSize=256):"image"===r&&(e="url",a.coordinates=t.coordinates);a[e]=n,t.sourceattribution&&(a.attribution=i(t.sourceattribution));return a}(t);e.addSource(this.idSource,r)}},l.findFollowingMapboxLayerId=function(t){if("traces"===t)for(var e=this.subplot.getMapLayers(),r=0;r1)for(r=0;r-1&&v(e.originalEvent,n,[r.xaxis],[r.yaxis],r.id,t),i.indexOf("event")>-1&&c.click(n,e.originalEvent)}}},_.updateFx=function(t){var e=this,r=e.map,n=e.gd;if(!e.isStatic){var a,o=t.dragmode;a=f(o)?function(t,r){(t.range={})[e.id]=[c([r.xmin,r.ymin]),c([r.xmax,r.ymax])]}:function(t,r,n){(t.lassoPoints={})[e.id]=n.filtered.map(c)};var s=e.dragOptions;e.dragOptions=i.extendDeep(s||{},{dragmode:t.dragmode,element:e.div,gd:n,plotinfo:{id:e.id,domain:t[e.id].domain,xaxis:e.xaxis,yaxis:e.yaxis,fillRangeItems:a},xaxes:[e.xaxis],yaxes:[e.yaxis],subplot:e.id}),r.off("click",e.onClickInPanHandler),p(o)||h(o)?(r.dragPan.disable(),r.on("zoomstart",e.clearSelect),e.dragOptions.prepFn=function(t,r,n){d(t,r,n,e.dragOptions,o)},l.init(e.dragOptions)):(r.dragPan.enable(),r.off("zoomstart",e.clearSelect),e.div.onmousedown=null,e.onClickInPanHandler=e.onClickInPanFn(e.dragOptions),r.on("click",e.onClickInPanHandler))}function c(t){var r=e.map.unproject(t);return[r.lng,r.lat]}},_.updateFramework=function(t){var e=t[this.id].domain,r=t._size,n=this.div.style;n.width=r.w*(e.x[1]-e.x[0])+"px",n.height=r.h*(e.y[1]-e.y[0])+"px",n.left=r.l+e.x[0]*r.w+"px",n.top=r.t+(1-e.y[1])*r.h+"px",this.xaxis._offset=r.l+e.x[0]*r.w,this.xaxis._length=r.w*(e.x[1]-e.x[0]),this.yaxis._offset=r.t+(1-e.y[1])*r.h,this.yaxis._length=r.h*(e.y[1]-e.y[0])},_.updateLayers=function(t){var e,r=t[this.id].layers,n=this.layerList;if(r.length!==n.length){for(e=0;e=e.width-20?(a["text-anchor"]="start",a.x=5):(a["text-anchor"]="end",a.x=e._paper.attr("width")-7),r.attr(a);var o=r.select(".js-link-to-tool"),s=r.select(".js-link-spacer"),l=r.select(".js-sourcelinks");t._context.showSources&&t._context.showSources(t),t._context.showLink&&function(t,e){e.text("");var r=e.append("a").attr({"xlink:xlink:href":"#",class:"link--impt link--embedview","font-weight":"bold"}).text(t._context.linkText+" "+String.fromCharCode(187));if(t._context.sendData)r.on("click",(function(){b.sendDataToCloud(t)}));else{var n=window.location.pathname.split("/"),i=window.location.search;r.attr({"xlink:xlink:show":"new","xlink:xlink:href":"/"+n[2].split(".")[0]+"/"+n[1]+i})}}(t,o),s.text(o.text()&&l.text()?" - ":"")}},b.sendDataToCloud=function(t){var e=(window.PLOTLYENV||{}).BASE_URL||t._context.plotlyServerURL;if(e){t.emit("plotly_beforeexport");var r=n.select(t).append("div").attr("id","hiddenform").style("display","none"),i=r.append("form").attr({action:e+"/external",method:"post",target:"_blank"});return i.append("input").attr({type:"text",name:"data"}).node().value=b.graphJson(t,!1,"keepdata"),i.node().submit(),r.remove(),t.emit("plotly_afterexport"),!1}};var T=["days","shortDays","months","shortMonths","periods","dateTime","date","time","decimal","thousands","grouping","currency"],k=["year","month","dayMonth","dayMonthYear"];function A(t,e){var r=t._context.locale;r||(r="en-US");var n=!1,i={};function a(t){for(var r=!0,a=0;a1&&z.length>1){for(s.getComponentMethod("grid","sizeDefaults")(c,l),o=0;o15&&z.length>15&&0===l.shapes.length&&0===l.images.length,b.linkSubplots(h,l,f,n),b.cleanPlot(h,l,f,n);var N=!(!n._has||!n._has("gl2d")),j=!(!l._has||!l._has("gl2d")),U=!(!n._has||!n._has("cartesian"))||N,V=!(!l._has||!l._has("cartesian"))||j;U&&!V?n._bgLayer.remove():V&&!U&&(l._shouldCreateBgLayer=!0),n._zoomlayer&&!t._dragging&&d({_fullLayout:n}),function(t,e){var r,n=[];e.meta&&(r=e._meta={meta:e.meta,layout:{meta:e.meta}});for(var i=0;i0){var f=1-2*s;n=Math.round(f*n),i=Math.round(f*i)}}var h=b.layoutAttributes.width.min,p=b.layoutAttributes.height.min;n1,m=!e.height&&Math.abs(r.height-i)>1;(m||d)&&(d&&(r.width=n),m&&(r.height=i)),t._initialAutoSize||(t._initialAutoSize={width:n,height:i}),b.sanitizeMargins(r)},b.supplyLayoutModuleDefaults=function(t,e,r,n){var i,a,o,l=s.componentsRegistry,c=e._basePlotModules,f=s.subplotsRegistry.cartesian;for(i in l)(o=l[i]).includeBasePlot&&o.includeBasePlot(t,e);for(var h in c.length||c.push(f),e._has("cartesian")&&(s.getComponentMethod("grid","contentDefaults")(t,e),f.finalizeSubplots(t,e)),e._subplots)e._subplots[h].sort(u.subplotSort);for(a=0;a1&&(r.l/=m,r.r/=m)}if(f){var g=(r.t+r.b)/f;g>1&&(r.t/=g,r.b/=g)}var v=void 0!==r.xl?r.xl:r.x,y=void 0!==r.xr?r.xr:r.x,x=void 0!==r.yt?r.yt:r.y,_=void 0!==r.yb?r.yb:r.y;h[e]={l:{val:v,size:r.l+d},r:{val:y,size:r.r+d},b:{val:_,size:r.b+d},t:{val:x,size:r.t+d}},p[e]=1}else delete h[e],delete p[e];if(!n._replotting)return b.doAutoMargin(t)}},b.doAutoMargin=function(t){var e=t._fullLayout,r=e.width,n=e.height;e._size||(e._size={}),C(e);var i=e._size,a=e.margin,l=u.extendFlat({},i),c=a.l,f=a.r,h=a.t,d=a.b,m=e._pushmargin,g=e._pushmarginIds;if(!1!==e.margin.autoexpand){for(var v in m)g[v]||delete m[v];for(var y in m.base={l:{val:0,size:c},r:{val:1,size:f},t:{val:1,size:h},b:{val:0,size:d}},m){var x=m[y].l||{},_=m[y].b||{},w=x.val,T=x.size,k=_.val,A=_.size;for(var M in m){if(o(T)&&m[M].r){var S=m[M].r.val,E=m[M].r.size;if(S>w){var L=(T*S+(E-r)*w)/(S-w),P=(E*(1-w)+(T-r)*(1-S))/(S-w);L+P>c+f&&(c=L,f=P)}}if(o(A)&&m[M].t){var I=m[M].t.val,O=m[M].t.size;if(I>k){var z=(A*I+(O-n)*k)/(I-k),D=(O*(1-k)+(A-n)*(1-I))/(I-k);z+D>d+h&&(d=z,h=D)}}}}}var R=u.constrain(r-a.l-a.r,2,64),F=u.constrain(n-a.t-a.b,2,64),B=Math.max(0,r-R),N=Math.max(0,n-F);if(B){var j=(c+f)/B;j>1&&(c/=j,f/=j)}if(N){var U=(d+h)/N;U>1&&(d/=U,h/=U)}if(i.l=Math.round(c),i.r=Math.round(f),i.t=Math.round(h),i.b=Math.round(d),i.p=Math.round(a.pad),i.w=Math.round(r)-i.l-i.r,i.h=Math.round(n)-i.t-i.b,!e._replotting&&b.didMarginChange(l,i)){"_redrawFromAutoMarginCount"in e?e._redrawFromAutoMarginCount++:e._redrawFromAutoMarginCount=1;var V=3*(1+Object.keys(g).length);if(e._redrawFromAutoMarginCount0&&(t._transitioningWithDuration=!0),t._transitionData._interruptCallbacks.push((function(){n=!0})),r.redraw&&t._transitionData._interruptCallbacks.push((function(){return s.call("redraw",t)})),t._transitionData._interruptCallbacks.push((function(){t.emit("plotly_transitioninterrupted",[])}));var a=0,o=0;function l(){return a++,function(){o++,n||o!==a||function(e){if(!t._transitionData)return;(function(t){if(t)for(;t.length;)t.shift()})(t._transitionData._interruptCallbacks),Promise.resolve().then((function(){if(r.redraw)return s.call("redraw",t)})).then((function(){t._transitioning=!1,t._transitioningWithDuration=!1,t.emit("plotly_transitioned",[])})).then(e)}(i)}}r.runFn(l),setTimeout(l())}))}],a=u.syncOrAsync(i,t);return a&&a.then||(a=Promise.resolve()),a.then((function(){return t}))}b.didMarginChange=function(t,e){for(var r=0;r1)return!0}return!1},b.graphJson=function(t,e,r,n,i,a){(i&&e&&!t._fullData||i&&!e&&!t._fullLayout)&&b.supplyDefaults(t);var o=i?t._fullData:t.data,s=i?t._fullLayout:t.layout,l=(t._transitionData||{})._frames;function c(t,e){if("function"==typeof t)return e?"_function_":null;if(u.isPlainObject(t)){var n,i={};return Object.keys(t).sort().forEach((function(a){if(-1===["_","["].indexOf(a.charAt(0)))if("function"!=typeof t[a]){if("keepdata"===r){if("src"===a.substr(a.length-3))return}else if("keepstream"===r){if("string"==typeof(n=t[a+"src"])&&n.indexOf(":")>0&&!u.isPlainObject(t.stream))return}else if("keepall"!==r&&"string"==typeof(n=t[a+"src"])&&n.indexOf(":")>0)return;i[a]=c(t[a],e)}else e&&(i[a]="_function")})),i}return Array.isArray(t)?t.map((function(t){return c(t,e)})):u.isTypedArray(t)?u.simpleMap(t,u.identity):u.isJSDate(t)?u.ms2DateTimeLocal(+t):t}var f={data:(o||[]).map((function(t){var r=c(t);return e&&delete r.fit,r}))};if(!e&&(f.layout=c(s),i)){var h=s._size;f.layout.computed={margin:{b:h.b,l:h.l,r:h.r,t:h.t}}}return l&&(f.frames=c(l)),a&&(f.config=c(t._context,!0)),"object"===n?f:JSON.stringify(f)},b.modifyFrames=function(t,e){var r,n,i,a=t._transitionData._frames,o=t._transitionData._frameHash;for(r=0;r=0;a--)if(s[a].enabled){r._indexToPoints=s[a]._indexToPoints;break}n&&n.calc&&(o=n.calc(t,r))}Array.isArray(o)&&o[0]||(o=[{x:h,y:h}]),o[0].t||(o[0].t={}),o[0].trace=r,d[e]=o}}for(z(o,c,f),i=0;i1e-10?t:0}function h(t,e,r){e=e||0,r=r||0;for(var n=t.length,i=new Array(n),a=0;a0?r:1/0})),i=n.mod(r+1,e.length);return[e[r],e[i]]},findIntersectionXY:c,findXYatLength:function(t,e,r,n){var i=-e*r,a=e*e+1,o=2*(e*i-r),s=i*i+r*r-t*t,l=Math.sqrt(o*o-4*a*s),c=(-o+l)/(2*a),u=(-o-l)/(2*a);return[[c,e*c+i+n],[u,e*u+i+n]]},clampTiny:f,pathPolygon:function(t,e,r,n,i,a){return"M"+h(u(t,e,r,n),i,a).join("L")},pathPolygonAnnulus:function(t,e,r,n,i,a,o){var s,l;t=90||s>90&&l>=450?1:u<=0&&h<=0?0:Math.max(u,h);e=s<=180&&l>=180||s>180&&l>=540?-1:c>=0&&f>=0?0:Math.min(c,f);r=s<=270&&l>=270||s>270&&l>=630?-1:u>=0&&h>=0?0:Math.min(u,h);n=l>=360?1:c<=0&&f<=0?0:Math.max(c,f);return[e,r,n,i]}(p),b=x[2]-x[0],_=x[3]-x[1],w=h/f,T=Math.abs(_/b);w>T?(d=f,y=(h-(m=f*T))/n.h/2,g=[o[0],o[1]],v=[s[0]+y,s[1]-y]):(m=h,y=(f-(d=h/T))/n.w/2,g=[o[0]+y,o[1]-y],v=[s[0],s[1]]),this.xLength2=d,this.yLength2=m,this.xDomain2=g,this.yDomain2=v;var k,A=this.xOffset2=n.l+n.w*g[0],M=this.yOffset2=n.t+n.h*(1-v[1]),S=this.radius=d/b,E=this.innerRadius=this.getHole(e)*S,L=this.cx=A-S*x[0],C=this.cy=M+S*x[3],P=this.cxx=L-A,I=this.cyy=C-M,O=i.side;"counterclockwise"===O?(k=O,O="top"):"clockwise"===O&&(k=O,O="bottom"),this.radialAxis=this.mockAxis(t,e,i,{_id:"x",side:O,_trueSide:k,domain:[E/n.w,S/n.w]}),this.angularAxis=this.mockAxis(t,e,a,{side:"right",domain:[0,Math.PI],autorange:!1}),this.doAutoRange(t,e),this.updateAngularAxis(t,e),this.updateRadialAxis(t,e),this.updateRadialAxisTitle(t,e),this.xaxis=this.mockCartesianAxis(t,e,{_id:"x",domain:g}),this.yaxis=this.mockCartesianAxis(t,e,{_id:"y",domain:v});var z=this.pathSubplot();this.clipPaths.forTraces.select("path").attr("d",z).attr("transform",l(P,I)),r.frontplot.attr("transform",l(A,M)).call(u.setClipUrl,this._hasClipOnAxisFalse?null:this.clipIds.forTraces,this.gd),r.bg.attr("d",z).attr("transform",l(L,C)).call(c.fill,e.bgcolor)},N.mockAxis=function(t,e,r,n){var i=o.extendFlat({},r,n);return d(i,e,t),i},N.mockCartesianAxis=function(t,e,r){var n=this,i=n.isSmith,a=r._id,s=o.extendFlat({type:"linear"},r);p(s,t);var l={x:[0,2],y:[1,3]};return s.setRange=function(){var t=n.sectorBBox,r=l[a],i=n.radialAxis._rl,o=(i[1]-i[0])/(1-n.getHole(e));s.range=[t[r[0]]*o,t[r[1]]*o]},s.isPtWithinRange="x"!==a||i?function(){return!0}:function(t){return n.isPtInside(t)},s.setRange(),s.setScale(),s},N.doAutoRange=function(t,e){var r=this.gd,n=this.radialAxis,i=this.getRadial(e);m(r,n);var a=n.range;i.range=a.slice(),i._input.range=a.slice(),n._rl=[n.r2l(a[0],null,"gregorian"),n.r2l(a[1],null,"gregorian")]},N.updateRadialAxis=function(t,e){var r=this,n=r.gd,i=r.layers,a=r.radius,u=r.innerRadius,f=r.cx,p=r.cy,d=r.getRadial(e),m=D(r.getSector(e)[0],360),g=r.radialAxis,v=u90&&m<=270&&(g.tickangle=180);var x=y?function(t){var e=O(r,C([t.x,0]));return l(e[0]-f,e[1]-p)}:function(t){return l(g.l2p(t.x)+u,0)},b=y?function(t){return I(r,t.x,-1/0,1/0)}:function(t){return r.pathArc(g.r2p(t.x)+u)},_=j(d);if(r.radialTickLayout!==_&&(i["radial-axis"].selectAll(".xtick").remove(),r.radialTickLayout=_),v){g.setScale();var w=0,T=y?(g.tickvals||[]).filter((function(t){return t>=0})).map((function(t){return h.tickText(g,t,!0,!1)})):h.calcTicks(g),k=y?T:h.clipEnds(g,T),A=h.getTickSigns(g)[2];y&&(("top"===g.ticks&&"bottom"===g.side||"bottom"===g.ticks&&"top"===g.side)&&(A=-A),"top"===g.ticks&&"top"===g.side&&(w=-g.ticklen),"bottom"===g.ticks&&"bottom"===g.side&&(w=g.ticklen)),h.drawTicks(n,g,{vals:T,layer:i["radial-axis"],path:h.makeTickPath(g,0,A),transFn:x,crisp:!1}),h.drawGrid(n,g,{vals:k,layer:i["radial-grid"],path:b,transFn:o.noop,crisp:!1}),h.drawLabels(n,g,{vals:T,layer:i["radial-axis"],transFn:x,labelFns:h.makeLabelFns(g,w)})}var M=r.radialAxisAngle=r.vangles?F(U(R(d.angle),r.vangles)):d.angle,S=l(f,p),E=S+s(-M);V(i["radial-axis"],v&&(d.showticklabels||d.ticks),{transform:E}),V(i["radial-grid"],v&&d.showgrid,{transform:y?"":S}),V(i["radial-line"].select("line"),v&&d.showline,{x1:y?-a:u,y1:0,x2:a,y2:0,transform:E}).attr("stroke-width",d.linewidth).call(c.stroke,d.linecolor)},N.updateRadialAxisTitle=function(t,e,r){if(!this.isSmith){var n=this.gd,i=this.radius,a=this.cx,o=this.cy,s=this.getRadial(e),l=this.id+"title",c=0;if(s.title){var f=u.bBox(this.layers["radial-axis"].node()).height,h=s.title.font.size,p=s.side;c="top"===p?h:"counterclockwise"===p?-(f+.4*h):f+.8*h}var d=void 0!==r?r:this.radialAxisAngle,m=R(d),g=Math.cos(m),v=Math.sin(m),y=a+i/2*g+c*v,b=o-i/2*v+c*g;this.layers["radial-axis-title"]=x.draw(n,l,{propContainer:s,propName:this.id+".radialaxis.title",placeholder:z(n,"Click to enter radial axis title"),attributes:{x:y,y:b,"text-anchor":"middle"},transform:{rotate:-d}})}},N.updateAngularAxis=function(t,e){var r=this,n=r.gd,i=r.layers,a=r.radius,u=r.innerRadius,f=r.cx,p=r.cy,d=r.getAngular(e),m=r.angularAxis,g=r.isSmith;g||(r.fillViewInitialKey("angularaxis.rotation",d.rotation),m.setGeometry(),m.setScale());var v=g?function(t){var e=O(r,C([0,t.x]));return Math.atan2(e[0]-f,e[1]-p)-Math.PI/2}:function(t){return m.t2g(t.x)};"linear"===m.type&&"radians"===m.thetaunit&&(m.tick0=F(m.tick0),m.dtick=F(m.dtick));var y=function(t){return l(f+a*Math.cos(t),p-a*Math.sin(t))},x=g?function(t){var e=O(r,C([0,t.x]));return l(e[0],e[1])}:function(t){return y(v(t))},b=g?function(t){var e=O(r,C([0,t.x])),n=Math.atan2(e[0]-f,e[1]-p)-Math.PI/2;return l(e[0],e[1])+s(-F(n))}:function(t){var e=v(t);return y(e)+s(-F(e))},_=g?function(t){return P(r,t.x,0,1/0)}:function(t){var e=v(t),r=Math.cos(e),n=Math.sin(e);return"M"+[f+u*r,p-u*n]+"L"+[f+a*r,p-a*n]},w=h.makeLabelFns(m,0).labelStandoff,T={xFn:function(t){var e=v(t);return Math.cos(e)*w},yFn:function(t){var e=v(t),r=Math.sin(e)>0?.2:1;return-Math.sin(e)*(w+t.fontSize*r)+Math.abs(Math.cos(e))*(t.fontSize*M)},anchorFn:function(t){var e=v(t),r=Math.cos(e);return Math.abs(r)<.1?"middle":r>0?"start":"end"},heightFn:function(t,e,r){var n=v(t);return-.5*(1+Math.sin(n))*r}},k=j(d);r.angularTickLayout!==k&&(i["angular-axis"].selectAll("."+m._id+"tick").remove(),r.angularTickLayout=k);var A,S=g?[1/0].concat(m.tickvals||[]).map((function(t){return h.tickText(m,t,!0,!1)})):h.calcTicks(m);if(g&&(S[0].text="\u221e",S[0].fontSize*=1.75),"linear"===e.gridshape?(A=S.map(v),o.angleDelta(A[0],A[1])<0&&(A=A.slice().reverse())):A=null,r.vangles=A,"category"===m.type&&(S=S.filter((function(t){return o.isAngleInsideSector(v(t),r.sectorInRad)}))),m.visible){var E="inside"===m.ticks?-1:1,L=(m.linewidth||1)/2;h.drawTicks(n,m,{vals:S,layer:i["angular-axis"],path:"M"+E*L+",0h"+E*m.ticklen,transFn:b,crisp:!1}),h.drawGrid(n,m,{vals:S,layer:i["angular-grid"],path:_,transFn:o.noop,crisp:!1}),h.drawLabels(n,m,{vals:S,layer:i["angular-axis"],repositionOnUpdate:!0,transFn:x,labelFns:T})}V(i["angular-line"].select("path"),d.showline,{d:r.pathSubplot(),transform:l(f,p)}).attr("stroke-width",d.linewidth).call(c.stroke,d.linecolor)},N.updateFx=function(t,e){this.gd._context.staticPlot||(!this.isSmith&&(this.updateAngularDrag(t),this.updateRadialDrag(t,e,0),this.updateRadialDrag(t,e,1)),this.updateHoverAndMainDrag(t))},N.updateHoverAndMainDrag=function(t){var e,r,s=this,c=s.isSmith,u=s.gd,f=s.layers,h=t._zoomlayer,p=S.MINZOOM,d=S.OFFEDGE,m=s.radius,x=s.innerRadius,T=s.cx,k=s.cy,A=s.cxx,M=s.cyy,L=s.sectorInRad,C=s.vangles,P=s.radialAxis,I=E.clampTiny,O=E.findXYatLength,z=E.findEnclosingVertexAngles,D=S.cornerHalfWidth,R=S.cornerLen/2,F=g.makeDragger(f,"path","maindrag",!1===t.dragmode?"none":"crosshair");n.select(F).attr("d",s.pathSubplot()).attr("transform",l(T,k)),F.onmousemove=function(t){y.hover(u,t,s.id),u._fullLayout._lasthover=F,u._fullLayout._hoversubplot=s.id},F.onmouseout=function(t){u._dragging||v.unhover(u,t)};var B,N,j,U,V,H,q,G,Y,W={element:F,gd:u,subplot:s.id,plotinfo:{id:s.id,xaxis:s.xaxis,yaxis:s.yaxis},xaxes:[s.xaxis],yaxes:[s.yaxis]};function X(t,e){return Math.sqrt(t*t+e*e)}function Z(t,e){return X(t-A,e-M)}function J(t,e){return Math.atan2(M-e,t-A)}function K(t,e){return[t*Math.cos(e),t*Math.sin(-e)]}function Q(t,e){if(0===t)return s.pathSector(2*D);var r=R/t,n=e-r,i=e+r,a=Math.max(0,Math.min(t,m)),o=a-D,l=a+D;return"M"+K(o,n)+"A"+[o,o]+" 0,0,0 "+K(o,i)+"L"+K(l,i)+"A"+[l,l]+" 0,0,1 "+K(l,n)+"Z"}function $(t,e,r){if(0===t)return s.pathSector(2*D);var n,i,a=K(t,e),o=K(t,r),l=I((a[0]+o[0])/2),c=I((a[1]+o[1])/2);if(l&&c){var u=c/l,f=-1/u,h=O(D,u,l,c);n=O(R,f,h[0][0],h[0][1]),i=O(R,f,h[1][0],h[1][1])}else{var p,d;c?(p=R,d=D):(p=D,d=R),n=[[l-p,c-d],[l+p,c-d]],i=[[l-p,c+d],[l+p,c+d]]}return"M"+n.join("L")+"L"+i.reverse().join("L")+"Z"}function tt(t,e){return e=Math.max(Math.min(e,m),x),tp?(t-1&&1===t&&_(e,u,[s.xaxis],[s.yaxis],s.id,W),r.indexOf("event")>-1&&y.click(u,e,s.id)}W.prepFn=function(t,n,a){var l=u._fullLayout.dragmode,f=F.getBoundingClientRect();u._fullLayout._calcInverseTransform(u);var p=u._fullLayout._invTransform;e=u._fullLayout._invScaleX,r=u._fullLayout._invScaleY;var d=o.apply3DTransform(p)(n-f.left,a-f.top);if(B=d[0],N=d[1],C){var v=E.findPolygonOffset(m,L[0],L[1],C);B+=A+v[0],N+=M+v[1]}switch(l){case"zoom":W.clickFn=st,c||(W.moveFn=C?it:rt,W.doneFn=at,function(){j=null,U=null,V=s.pathSubplot(),H=!1;var t=u._fullLayout[s.id];q=i(t.bgcolor).getLuminance(),(G=g.makeZoombox(h,q,T,k,V)).attr("fill-rule","evenodd"),Y=g.makeCorners(h,T,k),w(u)}());break;case"select":case"lasso":b(t,n,a,W,l)}},v.init(W)},N.updateRadialDrag=function(t,e,r){var i=this,c=i.gd,u=i.layers,f=i.radius,h=i.innerRadius,p=i.cx,d=i.cy,m=i.radialAxis,y=S.radialDragBoxSize,x=y/2;if(m.visible){var b,_,T,M=R(i.radialAxisAngle),E=m._rl,L=E[0],C=E[1],P=E[r],I=.75*(E[1]-E[0])/(1-i.getHole(e))/f;r?(b=p+(f+x)*Math.cos(M),_=d-(f+x)*Math.sin(M),T="radialdrag"):(b=p+(h-x)*Math.cos(M),_=d-(h-x)*Math.sin(M),T="radialdrag-inner");var O,z,D,B=g.makeRectDragger(u,T,"crosshair",-x,-x,y,y),N={element:B,gd:c};!1===t.dragmode&&(N.dragmode=!1),V(n.select(B),m.visible&&h0==(r?D>L:Dn?function(t){return t<=0}:function(t){return t>=0};t.c2g=function(r){var n=t.c2l(r)-e;return(s(n)?n:0)+o},t.g2c=function(r){return t.l2c(r+e-o)},t.g2p=function(t){return t*a},t.c2p=function(e){return t.g2p(t.c2g(e))}}}(t,e);break;case"angularaxis":!function(t,e){var r=t.type;if("linear"===r){var i=t.d2c,s=t.c2d;t.d2c=function(t,e){return function(t,e){return"degrees"===e?a(t):t}(i(t),e)},t.c2d=function(t,e){return s(function(t,e){return"degrees"===e?o(t):t}(t,e))}}t.makeCalcdata=function(e,i){var a,o,s=e[i],l=e._length,c=function(r){return t.d2c(r,e.thetaunit)};if(s){if(n.isTypedArray(s)&&"linear"===r){if(l===s.length)return s;if(s.subarray)return s.subarray(0,l)}for(a=new Array(l),o=0;o0?1:0}function i(t){var e=t[0],r=t[1];if(!isFinite(e)||!isFinite(r))return[1,0];var n=(e+1)*(e+1)+r*r;return[(e*e+r*r-1)/n,2*r/n]}function a(t,e){var r=e[0],n=e[1];return[r*t.radius+t.cx,-n*t.radius+t.cy]}function o(t,e){return e*t.radius}e.exports={smith:i,reactanceArc:function(t,e,r,n){var s=a(t,i([r,e])),l=s[0],c=s[1],u=a(t,i([n,e])),f=u[0],h=u[1];if(0===e)return["M"+l+","+c,"L"+f+","+h].join(" ");var p=o(t,1/Math.abs(e));return["M"+l+","+c,"A"+p+","+p+" 0 0,"+(e<0?1:0)+" "+f+","+h].join(" ")},resistanceArc:function(t,e,r,s){var l=o(t,1/(e+1)),c=a(t,i([e,r])),u=c[0],f=c[1],h=a(t,i([e,s])),p=h[0],d=h[1];if(n(r)!==n(s)){var m=a(t,i([e,0]));return["M"+u+","+f,"A"+l+","+l+" 0 0,"+(00){for(var n=[],i=0;i=u&&(h.min=0,d.min=0,g.min=0,t.aaxis&&delete t.aaxis.min,t.baxis&&delete t.baxis.min,t.caxis&&delete t.caxis.min)}function m(t,e,r,n){var i=h[e._name];function o(r,n){return a.coerce(t,e,i,r,n)}o("uirevision",n.uirevision),e.type="linear";var p=o("color"),d=p!==i.color.dflt?p:r.font.color,m=e._name.charAt(0).toUpperCase(),g="Component "+m,v=o("title.text",g);e._hovertitle=v===g?v:m,a.coerceFont(o,"title.font",{family:r.font.family,size:a.bigFont(r.font.size),color:d}),o("min"),u(t,e,o,"linear"),l(t,e,o,"linear"),s(t,e,o,"linear"),c(t,e,o,{outerTicks:!0}),o("showticklabels")&&(a.coerceFont(o,"tickfont",{family:r.font.family,size:r.font.size,color:d}),o("tickangle"),o("tickformat")),f(t,e,o,{dfltColor:p,bgColor:r.bgColor,blend:60,showLine:!0,showGrid:!0,noZeroLine:!0,attributes:i}),o("hoverformat"),o("layer")}e.exports=function(t,e,r){o(t,e,r,{type:"ternary",attributes:h,handleDefaults:d,font:e.font,paper_bgcolor:e.paper_bgcolor})}},{"../../components/color":366,"../../lib":503,"../../plot_api/plot_template":543,"../cartesian/line_grid_defaults":571,"../cartesian/prefix_suffix_defaults":573,"../cartesian/tick_label_defaults":578,"../cartesian/tick_mark_defaults":579,"../cartesian/tick_value_defaults":580,"../subplot_defaults":632,"./layout_attributes":635}],637:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("tinycolor2"),a=t("../../registry"),o=t("../../lib"),s=o.strTranslate,l=o._,c=t("../../components/color"),u=t("../../components/drawing"),f=t("../cartesian/set_convert"),h=t("../../lib/extend").extendFlat,p=t("../plots"),d=t("../cartesian/axes"),m=t("../../components/dragelement"),g=t("../../components/fx"),v=t("../../components/dragelement/helpers"),y=v.freeMode,x=v.rectMode,b=t("../../components/titles"),_=t("../cartesian/select").prepSelect,w=t("../cartesian/select").selectOnClick,T=t("../cartesian/select").clearSelect,k=t("../cartesian/select").clearSelectionsCache,A=t("../cartesian/constants");function M(t,e){this.id=t.id,this.graphDiv=t.graphDiv,this.init(e),this.makeFramework(e),this.aTickLayout=null,this.bTickLayout=null,this.cTickLayout=null}e.exports=M;var S=M.prototype;S.init=function(t){this.container=t._ternarylayer,this.defs=t._defs,this.layoutId=t._uid,this.traceHash={},this.layers={}},S.plot=function(t,e){var r=e[this.id],n=e._size;this._hasClipOnAxisFalse=!1;for(var i=0;iE*b?i=(a=b)*E:a=(i=x)/E,o=v*i/x,l=y*a/b,r=e.l+e.w*m-i/2,n=e.t+e.h*(1-g)-a/2,p.x0=r,p.y0=n,p.w=i,p.h=a,p.sum=_,p.xaxis={type:"linear",range:[w+2*k-_,_-w-2*T],domain:[m-o/2,m+o/2],_id:"x"},f(p.xaxis,p.graphDiv._fullLayout),p.xaxis.setScale(),p.xaxis.isPtWithinRange=function(t){return t.a>=p.aaxis.range[0]&&t.a<=p.aaxis.range[1]&&t.b>=p.baxis.range[1]&&t.b<=p.baxis.range[0]&&t.c>=p.caxis.range[1]&&t.c<=p.caxis.range[0]},p.yaxis={type:"linear",range:[w,_-T-k],domain:[g-l/2,g+l/2],_id:"y"},f(p.yaxis,p.graphDiv._fullLayout),p.yaxis.setScale(),p.yaxis.isPtWithinRange=function(){return!0};var A=p.yaxis.domain[0],M=p.aaxis=h({},t.aaxis,{range:[w,_-T-k],side:"left",tickangle:(+t.aaxis.tickangle||0)-30,domain:[A,A+l*E],anchor:"free",position:0,_id:"y",_length:i});f(M,p.graphDiv._fullLayout),M.setScale();var S=p.baxis=h({},t.baxis,{range:[_-w-k,T],side:"bottom",domain:p.xaxis.domain,anchor:"free",position:0,_id:"x",_length:i});f(S,p.graphDiv._fullLayout),S.setScale();var L=p.caxis=h({},t.caxis,{range:[_-w-T,k],side:"right",tickangle:(+t.caxis.tickangle||0)+30,domain:[A,A+l*E],anchor:"free",position:0,_id:"y",_length:i});f(L,p.graphDiv._fullLayout),L.setScale();var C="M"+r+","+(n+a)+"h"+i+"l-"+i/2+",-"+a+"Z";p.clipDef.select("path").attr("d",C),p.layers.plotbg.select("path").attr("d",C);var P="M0,"+a+"h"+i+"l-"+i/2+",-"+a+"Z";p.clipDefRelative.select("path").attr("d",P);var I=s(r,n);p.plotContainer.selectAll(".scatterlayer,.maplayer").attr("transform",I),p.clipDefRelative.select("path").attr("transform",null);var O=s(r-S._offset,n+a);p.layers.baxis.attr("transform",O),p.layers.bgrid.attr("transform",O);var z=s(r+i/2,n)+"rotate(30)"+s(0,-M._offset);p.layers.aaxis.attr("transform",z),p.layers.agrid.attr("transform",z);var D=s(r+i/2,n)+"rotate(-30)"+s(0,-L._offset);p.layers.caxis.attr("transform",D),p.layers.cgrid.attr("transform",D),p.drawAxes(!0),p.layers.aline.select("path").attr("d",M.showline?"M"+r+","+(n+a)+"l"+i/2+",-"+a:"M0,0").call(c.stroke,M.linecolor||"#000").style("stroke-width",(M.linewidth||0)+"px"),p.layers.bline.select("path").attr("d",S.showline?"M"+r+","+(n+a)+"h"+i:"M0,0").call(c.stroke,S.linecolor||"#000").style("stroke-width",(S.linewidth||0)+"px"),p.layers.cline.select("path").attr("d",L.showline?"M"+(r+i/2)+","+n+"l"+i/2+","+a:"M0,0").call(c.stroke,L.linecolor||"#000").style("stroke-width",(L.linewidth||0)+"px"),p.graphDiv._context.staticPlot||p.initInteractions(),u.setClipUrl(p.layers.frontplot,p._hasClipOnAxisFalse?null:p.clipId,p.graphDiv)},S.drawAxes=function(t){var e=this.graphDiv,r=this.id.substr(7)+"title",n=this.layers,i=this.aaxis,a=this.baxis,o=this.caxis;if(this.drawAx(i),this.drawAx(a),this.drawAx(o),t){var s=Math.max(i.showticklabels?i.tickfont.size/2:0,(o.showticklabels?.75*o.tickfont.size:0)+("outside"===o.ticks?.87*o.ticklen:0)),c=(a.showticklabels?a.tickfont.size:0)+("outside"===a.ticks?a.ticklen:0)+3;n["a-title"]=b.draw(e,"a"+r,{propContainer:i,propName:this.id+".aaxis.title",placeholder:l(e,"Click to enter Component A title"),attributes:{x:this.x0+this.w/2,y:this.y0-i.title.font.size/3-s,"text-anchor":"middle"}}),n["b-title"]=b.draw(e,"b"+r,{propContainer:a,propName:this.id+".baxis.title",placeholder:l(e,"Click to enter Component B title"),attributes:{x:this.x0-c,y:this.y0+this.h+.83*a.title.font.size+c,"text-anchor":"middle"}}),n["c-title"]=b.draw(e,"c"+r,{propContainer:o,propName:this.id+".caxis.title",placeholder:l(e,"Click to enter Component C title"),attributes:{x:this.x0+this.w+c,y:this.y0+this.h+.83*o.title.font.size+c,"text-anchor":"middle"}})}},S.drawAx=function(t){var e,r=this.graphDiv,n=t._name,i=n.charAt(0),a=t._id,s=this.layers[n],l=i+"tickLayout",c=(e=t).ticks+String(e.ticklen)+String(e.showticklabels);this[l]!==c&&(s.selectAll("."+a+"tick").remove(),this[l]=c),t.setScale();var u=d.calcTicks(t),f=d.clipEnds(t,u),h=d.makeTransTickFn(t),p=d.getTickSigns(t)[2],m=o.deg2rad(30),g=p*(t.linewidth||1)/2,v=p*t.ticklen,y=this.w,x=this.h,b="b"===i?"M0,"+g+"l"+Math.sin(m)*v+","+Math.cos(m)*v:"M"+g+",0l"+Math.cos(m)*v+","+-Math.sin(m)*v,_={a:"M0,0l"+x+",-"+y/2,b:"M0,0l-"+y/2+",-"+x,c:"M0,0l-"+x+","+y/2}[i];d.drawTicks(r,t,{vals:"inside"===t.ticks?f:u,layer:s,path:b,transFn:h,crisp:!1}),d.drawGrid(r,t,{vals:f,layer:this.layers[i+"grid"],path:_,transFn:h,crisp:!1}),d.drawLabels(r,t,{vals:u,layer:s,transFn:h,labelFns:d.makeLabelFns(t,0,30)})};var L=A.MINZOOM/2+.87,C="m-0.87,.5h"+L+"v3h-"+(L+5.2)+"l"+(L/2+2.6)+",-"+(.87*L+4.5)+"l2.6,1.5l-"+L/2+","+.87*L+"Z",P="m0.87,.5h-"+L+"v3h"+(L+5.2)+"l-"+(L/2+2.6)+",-"+(.87*L+4.5)+"l-2.6,1.5l"+L/2+","+.87*L+"Z",I="m0,1l"+L/2+","+.87*L+"l2.6,-1.5l-"+(L/2+2.6)+",-"+(.87*L+4.5)+"l-"+(L/2+2.6)+","+(.87*L+4.5)+"l2.6,1.5l"+L/2+",-"+.87*L+"Z",O=!0;function z(t){n.select(t).selectAll(".zoombox,.js-zoombox-backdrop,.js-zoombox-menu,.zoombox-corners").remove()}S.clearSelect=function(){k(this.dragOptions),T(this.dragOptions.gd)},S.initInteractions=function(){var t,e,r,n,f,h,p,d,v,b,T,k,M=this,S=M.layers.plotbg.select("path").node(),L=M.graphDiv,D=L._fullLayout._zoomlayer;function R(t){var e={};return e[M.id+".aaxis.min"]=t.a,e[M.id+".baxis.min"]=t.b,e[M.id+".caxis.min"]=t.c,e}function F(t,e){var r=L._fullLayout.clickmode;z(L),2===t&&(L.emit("plotly_doubleclick",null),a.call("_guiRelayout",L,R({a:0,b:0,c:0}))),r.indexOf("select")>-1&&1===t&&w(e,L,[M.xaxis],[M.yaxis],M.id,M.dragOptions),r.indexOf("event")>-1&&g.click(L,e,M.id)}function B(t,e){return 1-e/M.h}function N(t,e){return 1-(t+(M.h-e)/Math.sqrt(3))/M.w}function j(t,e){return(t-(M.h-e)/Math.sqrt(3))/M.w}function U(i,a){var o=r+i*t,s=n+a*e,l=Math.max(0,Math.min(1,B(0,n),B(0,s))),c=Math.max(0,Math.min(1,N(r,n),N(o,s))),u=Math.max(0,Math.min(1,j(r,n),j(o,s))),m=(l/2+u)*M.w,g=(1-l/2-c)*M.w,y=(m+g)/2,x=g-m,_=(1-l)*M.h,w=_-x/E;x.2?"rgba(0,0,0,0.4)":"rgba(255,255,255,0.3)").duration(200),k.transition().style("opacity",1).duration(200),b=!0),L.emit("plotly_relayouting",R(p))}function V(){z(L),p!==f&&(a.call("_guiRelayout",L,R(p)),O&&L.data&&L._context.showTips&&(o.notifier(l(L,"Double-click to zoom back out"),"long"),O=!1))}function H(t,e){var r=t/M.xaxis._m,n=e/M.yaxis._m,i=[(p={a:f.a-n,b:f.b+(r+n)/2,c:f.c-(r-n)/2}).a,p.b,p.c].sort(o.sorterAsc),a=i.indexOf(p.a),l=i.indexOf(p.b),c=i.indexOf(p.c);i[0]<0&&(i[1]+i[0]/2<0?(i[2]+=i[0]+i[1],i[0]=i[1]=0):(i[2]+=i[0]/2,i[1]+=i[0]/2,i[0]=0),p={a:i[a],b:i[l],c:i[c]},e=(f.a-p.a)*M.yaxis._m,t=(f.c-p.c-f.b+p.b)*M.xaxis._m);var h=s(M.x0+t,M.y0+e);M.plotContainer.selectAll(".scatterlayer,.maplayer").attr("transform",h);var d=s(-t,-e);M.clipDefRelative.select("path").attr("transform",d),M.aaxis.range=[p.a,M.sum-p.b-p.c],M.baxis.range=[M.sum-p.a-p.c,p.b],M.caxis.range=[M.sum-p.a-p.b,p.c],M.drawAxes(!1),M._hasClipOnAxisFalse&&M.plotContainer.select(".scatterlayer").selectAll(".trace").call(u.hideOutsideRangePoints,M),L.emit("plotly_relayouting",R(p))}function q(){a.call("_guiRelayout",L,R(p))}this.dragOptions={element:S,gd:L,plotinfo:{id:M.id,domain:L._fullLayout[M.id].domain,xaxis:M.xaxis,yaxis:M.yaxis},subplot:M.id,prepFn:function(a,l,u){M.dragOptions.xaxes=[M.xaxis],M.dragOptions.yaxes=[M.yaxis],t=L._fullLayout._invScaleX,e=L._fullLayout._invScaleY;var m=M.dragOptions.dragmode=L._fullLayout.dragmode;y(m)?M.dragOptions.minDrag=1:M.dragOptions.minDrag=void 0,"zoom"===m?(M.dragOptions.moveFn=U,M.dragOptions.clickFn=F,M.dragOptions.doneFn=V,function(t,e,a){var l=S.getBoundingClientRect();r=e-l.left,n=a-l.top,L._fullLayout._calcInverseTransform(L);var u=L._fullLayout._invTransform,m=o.apply3DTransform(u)(r,n);r=m[0],n=m[1],f={a:M.aaxis.range[0],b:M.baxis.range[1],c:M.caxis.range[1]},p=f,h=M.aaxis.range[1]-f.a,d=i(M.graphDiv._fullLayout[M.id].bgcolor).getLuminance(),v="M0,"+M.h+"L"+M.w/2+", 0L"+M.w+","+M.h+"Z",b=!1,T=D.append("path").attr("class","zoombox").attr("transform",s(M.x0,M.y0)).style({fill:d>.2?"rgba(0,0,0,0)":"rgba(255,255,255,0)","stroke-width":0}).attr("d",v),k=D.append("path").attr("class","zoombox-corners").attr("transform",s(M.x0,M.y0)).style({fill:c.background,stroke:c.defaultLine,"stroke-width":1,opacity:0}).attr("d","M0,0Z"),M.clearSelect(L)}(0,l,u)):"pan"===m?(M.dragOptions.moveFn=H,M.dragOptions.clickFn=F,M.dragOptions.doneFn=q,f={a:M.aaxis.range[0],b:M.baxis.range[1],c:M.caxis.range[1]},p=f,M.clearSelect(L)):(x(m)||y(m))&&_(a,l,u,M.dragOptions,m)}},S.onmousemove=function(t){g.hover(L,t,M.id),L._fullLayout._lasthover=S,L._fullLayout._hoversubplot=M.id},S.onmouseout=function(t){L._dragging||m.unhover(L,t)},m.init(this.dragOptions)}},{"../../components/color":366,"../../components/dragelement":385,"../../components/dragelement/helpers":384,"../../components/drawing":388,"../../components/fx":406,"../../components/titles":464,"../../lib":503,"../../lib/extend":493,"../../registry":638,"../cartesian/axes":554,"../cartesian/constants":561,"../cartesian/select":575,"../cartesian/set_convert":576,"../plots":619,"@plotly/d3":58,tinycolor2:312}],638:[function(t,e,r){"use strict";var n=t("./lib/loggers"),i=t("./lib/noop"),a=t("./lib/push_unique"),o=t("./lib/is_plain_object"),s=t("./lib/dom").addStyleRule,l=t("./lib/extend"),c=t("./plots/attributes"),u=t("./plots/layout_attributes"),f=l.extendFlat,h=l.extendDeepAll;function p(t){var e=t.name,i=t.categories,a=t.meta;if(r.modules[e])n.log("Type "+e+" already registered");else{r.subplotsRegistry[t.basePlotModule.name]||function(t){var e=t.name;if(r.subplotsRegistry[e])return void n.log("Plot type "+e+" already registered.");for(var i in v(t),r.subplotsRegistry[e]=t,r.componentsRegistry)b(i,t.name)}(t.basePlotModule);for(var o={},l=0;l-1&&(f[p[r]].title={text:""});for(r=0;r")?"":e.html(t).text()}));return e.remove(),r}(_),_=(_=_.replace(/&(?!\w+;|\#[0-9]+;| \#x[0-9A-F]+;)/g,"&")).replace(c,"'"),i.isIE()&&(_=(_=(_=_.replace(/"/gi,"'")).replace(/(\('#)([^']*)('\))/gi,'("#$2")')).replace(/(\\')/gi,'"')),_}},{"../components/color":366,"../components/drawing":388,"../constants/xmlns_namespaces":480,"../lib":503,"@plotly/d3":58}],647:[function(t,e,r){"use strict";var n=t("../../lib");e.exports=function(t,e){for(var r=0;rf+c||!n(u))}for(var p=0;pa))return e}return void 0!==r?r:t.dflt},r.coerceColor=function(t,e,r){return i(e).isValid()?e:void 0!==r?r:t.dflt},r.coerceEnumerated=function(t,e,r){return t.coerceNumber&&(e=+e),-1!==t.values.indexOf(e)?e:void 0!==r?r:t.dflt},r.getValue=function(t,e){var r;return Array.isArray(t)?e0?e+=r:u<0&&(e-=r)}return e}function z(t){var e=u,r=t.b,i=O(t);return n.inbox(r-e,i-e,_+(i-e)/(i-r)-1)}var D=t[f+"a"],R=t[h+"a"];m=Math.abs(D.r2c(D.range[1])-D.r2c(D.range[0]));var F=n.getDistanceFunction(i,p,d,(function(t){return(p(t)+d(t))/2}));if(n.getClosest(g,F,t),!1!==t.index&&g[t.index].p!==c){k||(L=function(t){return Math.min(A(t),t.p-y.bargroupwidth/2)},C=function(t){return Math.max(M(t),t.p+y.bargroupwidth/2)});var B=g[t.index],N=v.base?B.b+B.s:B.s;t[h+"0"]=t[h+"1"]=R.c2p(B[h],!0),t[h+"LabelVal"]=N;var j=y.extents[y.extents.round(B.p)];t[f+"0"]=D.c2p(x?L(B):j[0],!0),t[f+"1"]=D.c2p(x?C(B):j[1],!0);var U=void 0!==B.orig_p;return t[f+"LabelVal"]=U?B.orig_p:B.p,t.labelLabel=l(D,t[f+"LabelVal"],v[f+"hoverformat"]),t.valueLabel=l(R,t[h+"LabelVal"],v[h+"hoverformat"]),t.baseLabel=l(R,B.b,v[h+"hoverformat"]),t.spikeDistance=(function(t){var e=u,r=t.b,i=O(t);return n.inbox(r-e,i-e,w+(i-e)/(i-r)-1)}(B)+function(t){return P(A(t),M(t),w)}(B))/2,t[f+"Spike"]=D.c2p(B.p,!0),o(B,v,t),t.hovertemplate=v.hovertemplate,t}}function f(t,e){var r=e.mcc||t.marker.color,n=e.mlcc||t.marker.line.color,i=s(t,e);return a.opacity(r)?r:a.opacity(n)&&i?n:void 0}e.exports={hoverPoints:function(t,e,r,n,a){var o=u(t,e,r,n,a);if(o){var s=o.cd,l=s[0].trace,c=s[o.index];return o.color=f(l,c),i.getComponentMethod("errorbars","hoverInfo")(c,l,o),[o]}},hoverOnBars:u,getTraceColor:f}},{"../../components/color":366,"../../components/fx":406,"../../constants/numerical":479,"../../lib":503,"../../plots/cartesian/axes":554,"../../registry":638,"./helpers":654}],656:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),layoutAttributes:t("./layout_attributes"),supplyDefaults:t("./defaults").supplyDefaults,crossTraceDefaults:t("./defaults").crossTraceDefaults,supplyLayoutDefaults:t("./layout_defaults"),calc:t("./calc"),crossTraceCalc:t("./cross_trace_calc").crossTraceCalc,colorbar:t("../scatter/marker_colorbar"),arraysToCalcdata:t("./arrays_to_calcdata"),plot:t("./plot").plot,style:t("./style").style,styleOnSelect:t("./style").styleOnSelect,hoverPoints:t("./hover").hoverPoints,eventData:t("./event_data"),selectPoints:t("./select"),moduleType:"trace",name:"bar",basePlotModule:t("../../plots/cartesian"),categories:["bar-like","cartesian","svg","bar","oriented","errorBarsOK","showLegend","zoomScale"],animatable:!0,meta:{}}},{"../../plots/cartesian":568,"../scatter/marker_colorbar":945,"./arrays_to_calcdata":647,"./attributes":648,"./calc":649,"./cross_trace_calc":651,"./defaults":652,"./event_data":653,"./hover":655,"./layout_attributes":657,"./layout_defaults":658,"./plot":659,"./select":660,"./style":662}],657:[function(t,e,r){"use strict";e.exports={barmode:{valType:"enumerated",values:["stack","group","overlay","relative"],dflt:"group",editType:"calc"},barnorm:{valType:"enumerated",values:["","fraction","percent"],dflt:"",editType:"calc"},bargap:{valType:"number",min:0,max:1,editType:"calc"},bargroupgap:{valType:"number",min:0,max:1,dflt:0,editType:"calc"}}},{}],658:[function(t,e,r){"use strict";var n=t("../../registry"),i=t("../../plots/cartesian/axes"),a=t("../../lib"),o=t("./layout_attributes");e.exports=function(t,e,r){function s(r,n){return a.coerce(t,e,o,r,n)}for(var l=!1,c=!1,u=!1,f={},h=s("barmode"),p=0;p0}function S(t){return"auto"===t?0:t}function E(t,e){var r=Math.PI/180*e,n=Math.abs(Math.sin(r)),i=Math.abs(Math.cos(r));return{x:t.width*i+t.height*n,y:t.width*n+t.height*i}}function L(t,e,r,n,i,a){var o=!!a.isHorizontal,s=!!a.constrained,l=a.angle||0,c=a.anchor||"end",u="end"===c,f="start"===c,h=((a.leftToRight||0)+1)/2,p=1-h,d=i.width,m=i.height,g=Math.abs(e-t),v=Math.abs(n-r),y=g>2*_&&v>2*_?_:0;g-=2*y,v-=2*y;var x=S(l);"auto"!==l||d<=g&&m<=v||!(d>g||m>v)||(d>v||m>g)&&d.01?q:function(t,e,r){return r&&t===e?t:Math.abs(t-e)>=2?q(t):t>e?Math.ceil(t):Math.floor(t)};B=G(B,N,D),N=G(N,B,D),j=G(j,U,!D),U=G(U,j,!D)}var Y=A(a.ensureSingle(I,"path"),P,g,v);if(Y.style("vector-effect","non-scaling-stroke").attr("d",isNaN((N-B)*(U-j))||V&&t._context.staticPlot?"M0,0Z":"M"+B+","+j+"V"+U+"H"+N+"V"+j+"Z").call(l.setClipUrl,e.layerClipId,t),!P.uniformtext.mode&&R){var W=l.makePointStyleFns(f);l.singlePointStyle(c,Y,f,W,t)}!function(t,e,r,n,i,s,c,f,p,g,v){var w,T=e.xaxis,M=e.yaxis,C=t._fullLayout;function P(e,r,n){return a.ensureSingle(e,"text").text(r).attr({class:"bartext bartext-"+w,"text-anchor":"middle","data-notex":1}).call(l.font,n).call(o.convertToTspans,t)}var I=n[0].trace,O="h"===I.orientation,z=function(t,e,r,n,i){var o,s=e[0].trace;o=s.texttemplate?function(t,e,r,n,i){var o=e[0].trace,s=a.castOption(o,r,"texttemplate");if(!s)return"";var l,c,f,h,p="histogram"===o.type,d="waterfall"===o.type,m="funnel"===o.type,g="h"===o.orientation;g?(l="y",c=i,f="x",h=n):(l="x",c=n,f="y",h=i);function v(t){return u(h,h.c2l(t),!0).text}var y=e[r],x={};x.label=y.p,x.labelLabel=x[l+"Label"]=(_=y.p,u(c,c.c2l(_),!0).text);var _;var w=a.castOption(o,y.i,"text");(0===w||w)&&(x.text=w);x.value=y.s,x.valueLabel=x[f+"Label"]=v(y.s);var T={};b(T,o,y.i),(p||void 0===T.x)&&(T.x=g?x.value:x.label);(p||void 0===T.y)&&(T.y=g?x.label:x.value);(p||void 0===T.xLabel)&&(T.xLabel=g?x.valueLabel:x.labelLabel);(p||void 0===T.yLabel)&&(T.yLabel=g?x.labelLabel:x.valueLabel);d&&(x.delta=+y.rawS||y.s,x.deltaLabel=v(x.delta),x.final=y.v,x.finalLabel=v(x.final),x.initial=x.final-x.delta,x.initialLabel=v(x.initial));m&&(x.value=y.s,x.valueLabel=v(x.value),x.percentInitial=y.begR,x.percentInitialLabel=a.formatPercent(y.begR),x.percentPrevious=y.difR,x.percentPreviousLabel=a.formatPercent(y.difR),x.percentTotal=y.sumR,x.percenTotalLabel=a.formatPercent(y.sumR));var k=a.castOption(o,y.i,"customdata");k&&(x.customdata=k);return a.texttemplateString(s,x,t._d3locale,T,x,o._meta||{})}(t,e,r,n,i):s.textinfo?function(t,e,r,n){var i=t[0].trace,o="h"===i.orientation,s="waterfall"===i.type,l="funnel"===i.type;function c(t){return u(o?r:n,+t,!0).text}var f,h=i.textinfo,p=t[e],d=h.split("+"),m=[],g=function(t){return-1!==d.indexOf(t)};g("label")&&m.push((v=t[e].p,u(o?n:r,v,!0).text));var v;g("text")&&(0===(f=a.castOption(i,p.i,"text"))||f)&&m.push(f);if(s){var y=+p.rawS||p.s,x=p.v,b=x-y;g("initial")&&m.push(c(b)),g("delta")&&m.push(c(y)),g("final")&&m.push(c(x))}if(l){g("value")&&m.push(c(p.s));var _=0;g("percent initial")&&_++,g("percent previous")&&_++,g("percent total")&&_++;var w=_>1;g("percent initial")&&(f=a.formatPercent(p.begR),w&&(f+=" of initial"),m.push(f)),g("percent previous")&&(f=a.formatPercent(p.difR),w&&(f+=" of previous"),m.push(f)),g("percent total")&&(f=a.formatPercent(p.sumR),w&&(f+=" of total"),m.push(f))}return m.join("
")}(e,r,n,i):m.getValue(s.text,r);return m.coerceString(y,o)}(C,n,i,T,M);w=function(t,e){var r=m.getValue(t.textposition,e);return m.coerceEnumerated(x,r)}(I,i);var D="stack"===g.mode||"relative"===g.mode,R=n[i],F=!D||R._outmost;if(!z||"none"===w||(R.isBlank||s===c||f===p)&&("auto"===w||"inside"===w))return void r.select("text").remove();var B=C.font,N=d.getBarColor(n[i],I),j=d.getInsideTextFont(I,i,B,N),U=d.getOutsideTextFont(I,i,B),V=r.datum();O?"log"===T.type&&V.s0<=0&&(s=T.range[0]=G*(Z/Y):Z>=Y*(X/G);G>0&&Y>0&&(J||K||Q)?w="inside":(w="outside",H.remove(),H=null)}else w="inside";if(!H){W=a.ensureUniformFontSize(t,"outside"===w?U:j);var $=(H=P(r,z,W)).attr("transform");if(H.attr("transform",""),q=l.bBox(H.node()),G=q.width,Y=q.height,H.attr("transform",$),G<=0||Y<=0)return void H.remove()}var tt,et,rt=I.textangle;"outside"===w?(et="both"===I.constraintext||"outside"===I.constraintext,tt=function(t,e,r,n,i,a){var o,s=!!a.isHorizontal,l=!!a.constrained,c=a.angle||0,u=i.width,f=i.height,h=Math.abs(e-t),p=Math.abs(n-r);o=s?p>2*_?_:0:h>2*_?_:0;var d=1;l&&(d=s?Math.min(1,p/f):Math.min(1,h/u));var m=S(c),g=E(i,m),v=(s?g.x:g.y)/2,y=(i.left+i.right)/2,x=(i.top+i.bottom)/2,b=(t+e)/2,w=(r+n)/2,T=0,A=0,M=s?k(e,t):k(r,n);s?(b=e-M*o,T=M*v):(w=n+M*o,A=-M*v);return{textX:y,textY:x,targetX:b,targetY:w,anchorX:T,anchorY:A,scale:d,rotate:m}}(s,c,f,p,q,{isHorizontal:O,constrained:et,angle:rt})):(et="both"===I.constraintext||"inside"===I.constraintext,tt=L(s,c,f,p,q,{isHorizontal:O,constrained:et,angle:rt,anchor:I.insidetextanchor}));tt.fontSize=W.size,h("histogram"===I.type?"bar":I.type,tt,C),R.transform=tt,A(H,C,g,v).attr("transform",a.getTextTransform(tt))}(t,e,I,r,p,B,N,j,U,g,v),e.layerClipId&&l.hideOutsideRangePoint(c,I.select("text"),w,C,f.xcalendar,f.ycalendar)}));var j=!1===f.cliponaxis;l.setClipUrl(c,j?null:e.layerClipId,t)}));c.getComponentMethod("errorbars","plot")(t,I,e,g)},toMoveInsideBar:L}},{"../../components/color":366,"../../components/drawing":388,"../../components/fx/helpers":402,"../../lib":503,"../../lib/svg_text_utils":529,"../../plots/cartesian/axes":554,"../../registry":638,"./attributes":648,"./constants":650,"./helpers":654,"./style":662,"./uniform_text":664,"@plotly/d3":58,"fast-isnumeric":190}],660:[function(t,e,r){"use strict";function n(t,e,r,n,i){var a=e.c2p(n?t.s0:t.p0,!0),o=e.c2p(n?t.s1:t.p1,!0),s=r.c2p(n?t.p0:t.s0,!0),l=r.c2p(n?t.p1:t.s1,!0);return i?[(a+o)/2,(s+l)/2]:n?[o,(s+l)/2]:[(a+o)/2,l]}e.exports=function(t,e){var r,i=t.cd,a=t.xaxis,o=t.yaxis,s=i[0].trace,l="funnel"===s.type,c="h"===s.orientation,u=[];if(!1===e)for(r=0;r1||0===i.bargap&&0===i.bargroupgap&&!t[0].trace.marker.line.width)&&n.select(this).attr("shape-rendering","crispEdges")})),e.selectAll("g.points").each((function(e){d(n.select(this),e[0].trace,t)})),s.getComponentMethod("errorbars","style")(e)},styleTextPoints:m,styleOnSelect:function(t,e,r){var i=e[0].trace;i.selectedpoints?function(t,e,r){a.selectedPointStyle(t.selectAll("path"),e),function(t,e,r){t.each((function(t){var i,s=n.select(this);if(t.selected){i=o.ensureUniformFontSize(r,g(s,t,e,r));var l=e.selected.textfont&&e.selected.textfont.color;l&&(i.color=l),a.font(s,i)}else a.selectedTextStyle(s,e)}))}(t.selectAll("text"),e,r)}(r,i,t):(d(r,i,t),s.getComponentMethod("errorbars","style")(r))},getInsideTextFont:y,getOutsideTextFont:x,getBarColor:_,resizeText:l}},{"../../components/color":366,"../../components/drawing":388,"../../lib":503,"../../registry":638,"./attributes":648,"./helpers":654,"./uniform_text":664,"@plotly/d3":58}],663:[function(t,e,r){"use strict";var n=t("../../components/color"),i=t("../../components/colorscale/helpers").hasColorscale,a=t("../../components/colorscale/defaults"),o=t("../../lib").coercePattern;e.exports=function(t,e,r,s,l){var c=r("marker.color",s),u=i(t,"marker");u&&a(t,e,l,r,{prefix:"marker.",cLetter:"c"}),r("marker.line.color",n.defaultLine),i(t,"marker.line")&&a(t,e,l,r,{prefix:"marker.line.",cLetter:"c"}),r("marker.line.width"),r("marker.opacity"),o(r,"marker.pattern",c,u),r("selected.marker.color"),r("unselected.marker.color")}},{"../../components/color":366,"../../components/colorscale/defaults":376,"../../components/colorscale/helpers":377,"../../lib":503}],664:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../lib");function a(t){return"_"+t+"Text_minsize"}e.exports={recordMinTextSize:function(t,e,r){if(r.uniformtext.mode){var n=a(t),i=r.uniformtext.minsize,o=e.scale*e.fontSize;e.hide=oh.range[1]&&(x+=Math.PI);if(n.getClosest(c,(function(t){return m(y,x,[t.rp0,t.rp1],[t.thetag0,t.thetag1],d)?g+Math.min(1,Math.abs(t.thetag1-t.thetag0)/v)-1+(t.rp1-y)/(t.rp1-t.rp0)-1:1/0}),t),!1!==t.index){var b=c[t.index];t.x0=t.x1=b.ct[0],t.y0=t.y1=b.ct[1];var _=i.extendFlat({},b,{r:b.s,theta:b.p});return o(b,u,t),s(_,u,f,t),t.hovertemplate=u.hovertemplate,t.color=a(u,b),t.xLabelVal=t.yLabelVal=void 0,b.s<0&&(t.idealAlign="left"),[t]}}},{"../../components/fx":406,"../../lib":503,"../../plots/polar/helpers":621,"../bar/hover":655,"../scatterpolar/hover":1006}],669:[function(t,e,r){"use strict";e.exports={moduleType:"trace",name:"barpolar",basePlotModule:t("../../plots/polar"),categories:["polar","bar","showLegend"],attributes:t("./attributes"),layoutAttributes:t("./layout_attributes"),supplyDefaults:t("./defaults"),supplyLayoutDefaults:t("./layout_defaults"),calc:t("./calc").calc,crossTraceCalc:t("./calc").crossTraceCalc,plot:t("./plot"),colorbar:t("../scatter/marker_colorbar"),formatLabels:t("../scatterpolar/format_labels"),style:t("../bar/style").style,styleOnSelect:t("../bar/style").styleOnSelect,hoverPoints:t("./hover"),selectPoints:t("../bar/select"),meta:{}}},{"../../plots/polar":622,"../bar/select":660,"../bar/style":662,"../scatter/marker_colorbar":945,"../scatterpolar/format_labels":1005,"./attributes":665,"./calc":666,"./defaults":667,"./hover":668,"./layout_attributes":670,"./layout_defaults":671,"./plot":672}],670:[function(t,e,r){"use strict";e.exports={barmode:{valType:"enumerated",values:["stack","overlay"],dflt:"stack",editType:"calc"},bargap:{valType:"number",dflt:.1,min:0,max:1,editType:"calc"}}},{}],671:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("./layout_attributes");e.exports=function(t,e,r){var a,o={};function s(r,o){return n.coerce(t[a]||{},e[a],i,r,o)}for(var l=0;l0?(c=o,u=l):(c=l,u=o);var f=[s.findEnclosingVertexAngles(c,t.vangles)[0],(c+u)/2,s.findEnclosingVertexAngles(u,t.vangles)[1]];return s.pathPolygonAnnulus(n,i,c,u,f,e,r)};return function(t,n,i,o){return a.pathAnnulus(t,n,i,o,e,r)}}(e),p=e.layers.frontplot.select("g.barlayer");a.makeTraceGroups(p,r,"trace bars").each((function(){var r=n.select(this),s=a.ensureSingle(r,"g","points").selectAll("g.point").data(a.identity);s.enter().append("g").style("vector-effect","non-scaling-stroke").style("stroke-miterlimit",2).classed("point",!0),s.exit().remove(),s.each((function(t){var e,r=n.select(this),o=t.rp0=u.c2p(t.s0),s=t.rp1=u.c2p(t.s1),p=t.thetag0=f.c2g(t.p0),d=t.thetag1=f.c2g(t.p1);if(i(o)&&i(s)&&i(p)&&i(d)&&o!==s&&p!==d){var m=u.c2g(t.s1),g=(p+d)/2;t.ct=[l.c2p(m*Math.cos(g)),c.c2p(m*Math.sin(g))],e=h(o,s,p,d)}else e="M0,0Z";a.ensureSingle(r,"path").attr("d",e)})),o.setClipUrl(r,e._hasClipOnAxisFalse?e.clipIds.forTraces:null,t)}))}},{"../../components/drawing":388,"../../lib":503,"../../plots/polar/helpers":621,"@plotly/d3":58,"fast-isnumeric":190}],673:[function(t,e,r){"use strict";var n=t("../scatter/attributes"),i=t("../bar/attributes"),a=t("../../components/color/attributes"),o=t("../../plots/cartesian/axis_format_attributes").axisHoverFormat,s=t("../../plots/template_attributes").hovertemplateAttrs,l=t("../../lib/extend").extendFlat,c=n.marker,u=c.line;e.exports={y:{valType:"data_array",editType:"calc+clearAxisTypes"},x:{valType:"data_array",editType:"calc+clearAxisTypes"},x0:{valType:"any",editType:"calc+clearAxisTypes"},y0:{valType:"any",editType:"calc+clearAxisTypes"},dx:{valType:"number",editType:"calc"},dy:{valType:"number",editType:"calc"},xperiod:n.xperiod,yperiod:n.yperiod,xperiod0:n.xperiod0,yperiod0:n.yperiod0,xperiodalignment:n.xperiodalignment,yperiodalignment:n.yperiodalignment,xhoverformat:o("x"),yhoverformat:o("y"),name:{valType:"string",editType:"calc+clearAxisTypes"},q1:{valType:"data_array",editType:"calc+clearAxisTypes"},median:{valType:"data_array",editType:"calc+clearAxisTypes"},q3:{valType:"data_array",editType:"calc+clearAxisTypes"},lowerfence:{valType:"data_array",editType:"calc"},upperfence:{valType:"data_array",editType:"calc"},notched:{valType:"boolean",editType:"calc"},notchwidth:{valType:"number",min:0,max:.5,dflt:.25,editType:"calc"},notchspan:{valType:"data_array",editType:"calc"},boxpoints:{valType:"enumerated",values:["all","outliers","suspectedoutliers",!1],editType:"calc"},jitter:{valType:"number",min:0,max:1,editType:"calc"},pointpos:{valType:"number",min:-2,max:2,editType:"calc"},boxmean:{valType:"enumerated",values:[!0,"sd",!1],editType:"calc"},mean:{valType:"data_array",editType:"calc"},sd:{valType:"data_array",editType:"calc"},orientation:{valType:"enumerated",values:["v","h"],editType:"calc+clearAxisTypes"},quartilemethod:{valType:"enumerated",values:["linear","exclusive","inclusive"],dflt:"linear",editType:"calc"},width:{valType:"number",min:0,dflt:0,editType:"calc"},marker:{outliercolor:{valType:"color",dflt:"rgba(0, 0, 0, 0)",editType:"style"},symbol:l({},c.symbol,{arrayOk:!1,editType:"plot"}),opacity:l({},c.opacity,{arrayOk:!1,dflt:1,editType:"style"}),size:l({},c.size,{arrayOk:!1,editType:"calc"}),color:l({},c.color,{arrayOk:!1,editType:"style"}),line:{color:l({},u.color,{arrayOk:!1,dflt:a.defaultLine,editType:"style"}),width:l({},u.width,{arrayOk:!1,dflt:0,editType:"style"}),outliercolor:{valType:"color",editType:"style"},outlierwidth:{valType:"number",min:0,dflt:1,editType:"style"},editType:"style"},editType:"plot"},line:{color:{valType:"color",editType:"style"},width:{valType:"number",min:0,dflt:2,editType:"style"},editType:"plot"},fillcolor:n.fillcolor,whiskerwidth:{valType:"number",min:0,max:1,dflt:.5,editType:"calc"},offsetgroup:i.offsetgroup,alignmentgroup:i.alignmentgroup,selected:{marker:n.selected.marker,editType:"style"},unselected:{marker:n.unselected.marker,editType:"style"},text:l({},n.text,{}),hovertext:l({},n.hovertext,{}),hovertemplate:s({}),hoveron:{valType:"flaglist",flags:["boxes","points"],dflt:"boxes+points",editType:"style"}}},{"../../components/color/attributes":365,"../../lib/extend":493,"../../plots/cartesian/axis_format_attributes":557,"../../plots/template_attributes":633,"../bar/attributes":648,"../scatter/attributes":927}],674:[function(t,e,r){"use strict";var n=t("fast-isnumeric"),i=t("../../plots/cartesian/axes"),a=t("../../plots/cartesian/align_period"),o=t("../../lib"),s=t("../../constants/numerical").BADNUM,l=o._;e.exports=function(t,e){var r,c,y,x,b,_,w,T=t._fullLayout,k=i.getFromId(t,e.xaxis||"x"),A=i.getFromId(t,e.yaxis||"y"),M=[],S="violin"===e.type?"_numViolins":"_numBoxes";"h"===e.orientation?(y=k,x="x",b=A,_="y",w=!!e.yperiodalignment):(y=A,x="y",b=k,_="x",w=!!e.xperiodalignment);var E,L,C,P,I,O,z=function(t,e,r,i){var s,l=e+"0"in t,c="d"+e in t;if(e in t||l&&c){var u=r.makeCalcdata(t,e);return[a(t,r,e,u).vals,u]}s=l?t[e+"0"]:"name"in t&&("category"===r.type||n(t.name)&&-1!==["linear","log"].indexOf(r.type)||o.isDateTime(t.name)&&"date"===r.type)?t.name:i;for(var f="multicategory"===r.type?r.r2c_just_indices(s):r.d2c(s,0,t[e+"calendar"]),h=t._length,p=new Array(h),d=0;dE.uf};if(e._hasPreCompStats){var U=e[x],V=function(t){return y.d2c((e[t]||[])[r])},H=1/0,q=-1/0;for(r=0;r=E.q1&&E.q3>=E.med){var Y=V("lowerfence");E.lf=Y!==s&&Y<=E.q1?Y:p(E,C,P);var W=V("upperfence");E.uf=W!==s&&W>=E.q3?W:d(E,C,P);var X=V("mean");E.mean=X!==s?X:P?o.mean(C,P):(E.q1+E.q3)/2;var Z=V("sd");E.sd=X!==s&&Z>=0?Z:P?o.stdev(C,P,E.mean):E.q3-E.q1,E.lo=m(E),E.uo=g(E);var J=V("notchspan");J=J!==s&&J>0?J:v(E,P),E.ln=E.med-J,E.un=E.med+J;var K=E.lf,Q=E.uf;e.boxpoints&&C.length&&(K=Math.min(K,C[0]),Q=Math.max(Q,C[P-1])),e.notched&&(K=Math.min(K,E.ln),Q=Math.max(Q,E.un)),E.min=K,E.max=Q}else{var $;o.warn(["Invalid input - make sure that q1 <= median <= q3","q1 = "+E.q1,"median = "+E.med,"q3 = "+E.q3].join("\n")),$=E.med!==s?E.med:E.q1!==s?E.q3!==s?(E.q1+E.q3)/2:E.q1:E.q3!==s?E.q3:0,E.med=$,E.q1=E.q3=$,E.lf=E.uf=$,E.mean=E.sd=$,E.ln=E.un=$,E.min=E.max=$}H=Math.min(H,E.min),q=Math.max(q,E.max),E.pts2=L.filter(j),M.push(E)}}e._extremes[y._id]=i.findExtremes(y,[H,q],{padded:!0})}else{var tt=y.makeCalcdata(e,x),et=function(t,e){for(var r=t.length,n=new Array(r+1),i=0;i=0&&it0){var ut,ft;if((E={}).pos=E[_]=B[r],L=E.pts=nt[r].sort(f),P=(C=E[x]=L.map(h)).length,E.min=C[0],E.max=C[P-1],E.mean=o.mean(C,P),E.sd=o.stdev(C,P,E.mean),E.med=o.interp(C,.5),P%2&&(lt||ct))lt?(ut=C.slice(0,P/2),ft=C.slice(P/2+1)):ct&&(ut=C.slice(0,P/2+1),ft=C.slice(P/2)),E.q1=o.interp(ut,.5),E.q3=o.interp(ft,.5);else E.q1=o.interp(C,.25),E.q3=o.interp(C,.75);E.lf=p(E,C,P),E.uf=d(E,C,P),E.lo=m(E),E.uo=g(E);var ht=v(E,P);E.ln=E.med-ht,E.un=E.med+ht,at=Math.min(at,E.ln),ot=Math.max(ot,E.un),E.pts2=L.filter(j),M.push(E)}e._extremes[y._id]=i.findExtremes(y,e.notched?tt.concat([at,ot]):tt,{padded:!0})}return function(t,e){if(o.isArrayOrTypedArray(e.selectedpoints))for(var r=0;r0?(M[0].t={num:T[S],dPos:N,posLetter:_,valLetter:x,labels:{med:l(t,"median:"),min:l(t,"min:"),q1:l(t,"q1:"),q3:l(t,"q3:"),max:l(t,"max:"),mean:"sd"===e.boxmean?l(t,"mean \xb1 \u03c3:"):l(t,"mean:"),lf:l(t,"lower fence:"),uf:l(t,"upper fence:")}},T[S]++,M):[{t:{empty:!0}}]};var c={text:"tx",hovertext:"htx"};function u(t,e,r){for(var n in c)o.isArrayOrTypedArray(e[n])&&(Array.isArray(r)?o.isArrayOrTypedArray(e[n][r[0]])&&(t[c[n]]=e[n][r[0]][r[1]]):t[c[n]]=e[n][r])}function f(t,e){return t.v-e.v}function h(t){return t.v}function p(t,e,r){return 0===r?t.q1:Math.min(t.q1,e[Math.min(o.findBin(2.5*t.q1-1.5*t.q3,e,!0)+1,r-1)])}function d(t,e,r){return 0===r?t.q3:Math.max(t.q3,e[Math.max(o.findBin(2.5*t.q3-1.5*t.q1,e),0)])}function m(t){return 4*t.q1-3*t.q3}function g(t){return 4*t.q3-3*t.q1}function v(t,e){return 0===e?0:1.57*(t.q3-t.q1)/Math.sqrt(e)}},{"../../constants/numerical":479,"../../lib":503,"../../plots/cartesian/align_period":551,"../../plots/cartesian/axes":554,"fast-isnumeric":190}],675:[function(t,e,r){"use strict";var n=t("../../plots/cartesian/axes"),i=t("../../lib"),a=t("../../plots/cartesian/constraints").getAxisGroup,o=["v","h"];function s(t,e,r,o){var s,l,c,u=e.calcdata,f=e._fullLayout,h=o._id,p=h.charAt(0),d=[],m=0;for(s=0;s1,b=1-f[t+"gap"],_=1-f[t+"groupgap"];for(s=0;s0){var q=E.pointpos,G=E.jitter,Y=E.marker.size/2,W=0;q+G>=0&&((W=V*(q+G))>M?(H=!0,j=Y,B=W):W>R&&(j=Y,B=M)),W<=M&&(B=M);var X=0;q-G<=0&&((X=-V*(q-G))>S?(H=!0,U=Y,N=X):X>F&&(U=Y,N=S)),X<=S&&(N=S)}else B=M,N=S;var Z=new Array(c.length);for(l=0;l0?(g="v",v=x>0?Math.min(_,b):Math.min(b)):x>0?(g="h",v=Math.min(_)):v=0;if(v){e._length=v;var S=r("orientation",g);e._hasPreCompStats?"v"===S&&0===x?(r("x0",0),r("dx",1)):"h"===S&&0===y&&(r("y0",0),r("dy",1)):"v"===S&&0===x?r("x0"):"h"===S&&0===y&&r("y0"),i.getComponentMethod("calendars","handleTraceDefaults")(t,e,["x","y"],a)}else e.visible=!1}function f(t,e,r,i){var a=i.prefix,o=n.coerce2(t,e,c,"marker.outliercolor"),s=r("marker.line.outliercolor"),l="outliers";e._hasPreCompStats?l="all":(o||s)&&(l="suspectedoutliers");var u=r(a+"points",l);u?(r("jitter","all"===u?.3:0),r("pointpos","all"===u?-1.5:0),r("marker.symbol"),r("marker.opacity"),r("marker.size"),r("marker.color",e.line.color),r("marker.line.color"),r("marker.line.width"),"suspectedoutliers"===u&&(r("marker.line.outliercolor",e.marker.color),r("marker.line.outlierwidth")),r("selected.marker.color"),r("unselected.marker.color"),r("selected.marker.size"),r("unselected.marker.size"),r("text"),r("hovertext")):delete e.marker;var f=r("hoveron");"all"!==f&&-1===f.indexOf("points")||r("hovertemplate"),n.coerceSelectionMarkerOpacity(e,r)}e.exports={supplyDefaults:function(t,e,r,i){function s(r,i){return n.coerce(t,e,c,r,i)}if(u(t,e,s,i),!1!==e.visible){o(t,e,i,s),s("xhoverformat"),s("yhoverformat");var l=e._hasPreCompStats;l&&(s("lowerfence"),s("upperfence")),s("line.color",(t.marker||{}).color||r),s("line.width"),s("fillcolor",a.addOpacity(e.line.color,.5));var h=!1;if(l){var p=s("mean"),d=s("sd");p&&p.length&&(h=!0,d&&d.length&&(h="sd"))}s("boxmean",h),s("whiskerwidth"),s("width"),s("quartilemethod");var m=!1;if(l){var g=s("notchspan");g&&g.length&&(m=!0)}else n.validate(t.notchwidth,c.notchwidth)&&(m=!0);s("notched",m)&&s("notchwidth"),f(t,e,s,{prefix:"box"})}},crossTraceDefaults:function(t,e){var r,i;function a(t){return n.coerce(i._input,i,c,t)}for(var o=0;ot.lo&&(x.so=!0)}return a}));h.enter().append("path").classed("point",!0),h.exit().remove(),h.call(a.translatePoints,o,s)}function l(t,e,r,a){var o,s,l=e.val,c=e.pos,u=!!c.rangebreaks,f=a.bPos,h=a.bPosPxOffset||0,p=r.boxmean||(r.meanline||{}).visible;Array.isArray(a.bdPos)?(o=a.bdPos[0],s=a.bdPos[1]):(o=a.bdPos,s=a.bdPos);var d=t.selectAll("path.mean").data("box"===r.type&&r.boxmean||"violin"===r.type&&r.box.visible&&r.meanline.visible?i.identity:[]);d.enter().append("path").attr("class","mean").style({fill:"none","vector-effect":"non-scaling-stroke"}),d.exit().remove(),d.each((function(t){var e=c.c2l(t.pos+f,!0),i=c.l2p(e-o)+h,a=c.l2p(e+s)+h,d=u?(i+a)/2:c.l2p(e)+h,m=l.c2p(t.mean,!0),g=l.c2p(t.mean-t.sd,!0),v=l.c2p(t.mean+t.sd,!0);"h"===r.orientation?n.select(this).attr("d","M"+m+","+i+"V"+a+("sd"===p?"m0,0L"+g+","+d+"L"+m+","+i+"L"+v+","+d+"Z":"")):n.select(this).attr("d","M"+i+","+m+"H"+a+("sd"===p?"m0,0L"+d+","+g+"L"+i+","+m+"L"+d+","+v+"Z":""))}))}e.exports={plot:function(t,e,r,a){var c=e.xaxis,u=e.yaxis;i.makeTraceGroups(a,r,"trace boxes").each((function(t){var e,r,i=n.select(this),a=t[0],f=a.t,h=a.trace;(f.wdPos=f.bdPos*h.whiskerwidth,!0!==h.visible||f.empty)?i.remove():("h"===h.orientation?(e=u,r=c):(e=c,r=u),o(i,{pos:e,val:r},h,f),s(i,{x:c,y:u},h,f),l(i,{pos:e,val:r},h,f))}))},plotBoxAndWhiskers:o,plotPoints:s,plotBoxMean:l}},{"../../components/drawing":388,"../../lib":503,"@plotly/d3":58}],683:[function(t,e,r){"use strict";e.exports=function(t,e){var r,n,i=t.cd,a=t.xaxis,o=t.yaxis,s=[];if(!1===e)for(r=0;r=10)return null;for(var i=1/0,a=-1/0,o=e.length,s=0;s0?Math.floor:Math.ceil,I=L>0?Math.ceil:Math.floor,O=L>0?Math.min:Math.max,z=L>0?Math.max:Math.min,D=P(S+C),R=I(E-C),F=[[f=M(S)]];for(a=D;a*L=0;i--)a[u-i]=t[f][i],o[u-i]=e[f][i];for(s.push({x:a,y:o,bicubic:l}),i=f,a=[],o=[];i>=0;i--)a[f-i]=t[i][0],o[f-i]=e[i][0];return s.push({x:a,y:o,bicubic:c}),s}},{}],697:[function(t,e,r){"use strict";var n=t("../../plots/cartesian/axes"),i=t("../../lib/extend").extendFlat;e.exports=function(t,e,r){var a,o,s,l,c,u,f,h,p,d,m,g,v,y,x=t["_"+e],b=t[e+"axis"],_=b._gridlines=[],w=b._minorgridlines=[],T=b._boundarylines=[],k=t["_"+r],A=t[r+"axis"];"array"===b.tickmode&&(b.tickvals=x.slice());var M=t._xctrl,S=t._yctrl,E=M[0].length,L=M.length,C=t._a.length,P=t._b.length;n.prepTicks(b),"array"===b.tickmode&&delete b.tickvals;var I=b.smoothing?3:1;function O(n){var i,a,o,s,l,c,u,f,p,d,m,g,v=[],y=[],x={};if("b"===e)for(a=t.b2j(n),o=Math.floor(Math.max(0,Math.min(P-2,a))),s=a-o,x.length=P,x.crossLength=C,x.xy=function(e){return t.evalxy([],e,a)},x.dxy=function(e,r){return t.dxydi([],e,o,r,s)},i=0;i0&&(p=t.dxydi([],i-1,o,0,s),v.push(l[0]+p[0]/3),y.push(l[1]+p[1]/3),d=t.dxydi([],i-1,o,1,s),v.push(f[0]-d[0]/3),y.push(f[1]-d[1]/3)),v.push(f[0]),y.push(f[1]),l=f;else for(i=t.a2i(n),c=Math.floor(Math.max(0,Math.min(C-2,i))),u=i-c,x.length=C,x.crossLength=P,x.xy=function(e){return t.evalxy([],i,e)},x.dxy=function(e,r){return t.dxydj([],c,e,u,r)},a=0;a0&&(m=t.dxydj([],c,a-1,u,0),v.push(l[0]+m[0]/3),y.push(l[1]+m[1]/3),g=t.dxydj([],c,a-1,u,1),v.push(f[0]-g[0]/3),y.push(f[1]-g[1]/3)),v.push(f[0]),y.push(f[1]),l=f;return x.axisLetter=e,x.axis=b,x.crossAxis=A,x.value=n,x.constvar=r,x.index=h,x.x=v,x.y=y,x.smoothing=A.smoothing,x}function z(n){var i,a,o,s,l,c=[],u=[],f={};if(f.length=x.length,f.crossLength=k.length,"b"===e)for(o=Math.max(0,Math.min(P-2,n)),l=Math.min(1,Math.max(0,n-o)),f.xy=function(e){return t.evalxy([],e,n)},f.dxy=function(e,r){return t.dxydi([],e,o,r,l)},i=0;ix.length-1||_.push(i(z(o),{color:b.gridcolor,width:b.gridwidth,dash:b.griddash}));for(h=u;hx.length-1||m<0||m>x.length-1))for(g=x[s],v=x[m],a=0;ax[x.length-1]||w.push(i(O(d),{color:b.minorgridcolor,width:b.minorgridwidth,dash:b.minorgriddash}));b.startline&&T.push(i(z(0),{color:b.startlinecolor,width:b.startlinewidth})),b.endline&&T.push(i(z(x.length-1),{color:b.endlinecolor,width:b.endlinewidth}))}else{for(l=5e-15,u=(c=[Math.floor((x[x.length-1]-b.tick0)/b.dtick*(1+l)),Math.ceil((x[0]-b.tick0)/b.dtick/(1+l))].sort((function(t,e){return t-e})))[0],f=c[1],h=u;h<=f;h++)p=b.tick0+b.dtick*h,_.push(i(O(p),{color:b.gridcolor,width:b.gridwidth,dash:b.griddash}));for(h=u-1;hx[x.length-1]||w.push(i(O(d),{color:b.minorgridcolor,width:b.minorgridwidth,dash:b.minorgriddash}));b.startline&&T.push(i(O(x[0]),{color:b.startlinecolor,width:b.startlinewidth})),b.endline&&T.push(i(O(x[x.length-1]),{color:b.endlinecolor,width:b.endlinewidth}))}}},{"../../lib/extend":493,"../../plots/cartesian/axes":554}],698:[function(t,e,r){"use strict";var n=t("../../plots/cartesian/axes"),i=t("../../lib/extend").extendFlat;e.exports=function(t,e){var r,a,o,s=e._labels=[],l=e._gridlines;for(r=0;re.length&&(t=t.slice(0,e.length)):t=[],i=0;i90&&(p-=180,l=-l),{angle:p,flip:l,p:t.c2p(n,e,r),offsetMultplier:c}}},{}],712:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../components/drawing"),a=t("./map_1d_array"),o=t("./makepath"),s=t("./orient_text"),l=t("../../lib/svg_text_utils"),c=t("../../lib"),u=c.strRotate,f=c.strTranslate,h=t("../../constants/alignment");function p(t,e,r,s,l,c){var u="const-"+l+"-lines",f=r.selectAll("."+u).data(c);f.enter().append("path").classed(u,!0).style("vector-effect","non-scaling-stroke"),f.each((function(r){var s=r,l=s.x,c=s.y,u=a([],l,t.c2p),f=a([],c,e.c2p),h="M"+o(u,f,s.smoothing);n.select(this).attr("d",h).style("stroke-width",s.width).style("stroke",s.color).style("stroke-dasharray",i.dashStyle(s.dash,s.width)).style("fill","none")})),f.exit().remove()}function d(t,e,r,a,o,c,h,p){var d=c.selectAll("text."+p).data(h);d.enter().append("text").classed(p,!0);var m=0,g={};return d.each((function(o,c){var h;if("auto"===o.axis.tickangle)h=s(a,e,r,o.xy,o.dxy);else{var p=(o.axis.tickangle+180)*Math.PI/180;h=s(a,e,r,o.xy,[Math.cos(p),Math.sin(p)])}c||(g={angle:h.angle,flip:h.flip});var d=(o.endAnchor?-1:1)*h.flip,v=n.select(this).attr({"text-anchor":d>0?"start":"end","data-notex":1}).call(i.font,o.font).text(o.text).call(l.convertToTspans,t),y=i.bBox(this);v.attr("transform",f(h.p[0],h.p[1])+u(h.angle)+f(o.axis.labelpadding*d,.3*y.height)),m=Math.max(m,y.width+o.axis.labelpadding)})),d.exit().remove(),g.maxExtent=m,g}e.exports=function(t,e,r,i){var l=e.xaxis,u=e.yaxis,f=t._fullLayout._clips;c.makeTraceGroups(i,r,"trace").each((function(e){var r=n.select(this),i=e[0],h=i.trace,m=h.aaxis,g=h.baxis,y=c.ensureSingle(r,"g","minorlayer"),x=c.ensureSingle(r,"g","majorlayer"),b=c.ensureSingle(r,"g","boundarylayer"),_=c.ensureSingle(r,"g","labellayer");r.style("opacity",h.opacity),p(l,u,x,m,"a",m._gridlines),p(l,u,x,g,"b",g._gridlines),p(l,u,y,m,"a",m._minorgridlines),p(l,u,y,g,"b",g._minorgridlines),p(l,u,b,m,"a-boundary",m._boundarylines),p(l,u,b,g,"b-boundary",g._boundarylines);var w=d(t,l,u,h,i,_,m._labels,"a-label"),T=d(t,l,u,h,i,_,g._labels,"b-label");!function(t,e,r,n,i,a,o,l){var u,f,h,p,d=c.aggNums(Math.min,null,r.a),m=c.aggNums(Math.max,null,r.a),g=c.aggNums(Math.min,null,r.b),y=c.aggNums(Math.max,null,r.b);u=.5*(d+m),f=g,h=r.ab2xy(u,f,!0),p=r.dxyda_rough(u,f),void 0===o.angle&&c.extendFlat(o,s(r,i,a,h,r.dxydb_rough(u,f)));v(t,e,r,n,h,p,r.aaxis,i,a,o,"a-title"),u=d,f=.5*(g+y),h=r.ab2xy(u,f,!0),p=r.dxydb_rough(u,f),void 0===l.angle&&c.extendFlat(l,s(r,i,a,h,r.dxyda_rough(u,f)));v(t,e,r,n,h,p,r.baxis,i,a,l,"b-title")}(t,_,h,i,l,u,w,T),function(t,e,r,n,i){var s,l,u,f,h=r.select("#"+t._clipPathId);h.size()||(h=r.append("clipPath").classed("carpetclip",!0));var p=c.ensureSingle(h,"path","carpetboundary"),d=e.clipsegments,m=[];for(f=0;f90&&y<270,b=n.select(this);b.text(h.title.text).call(l.convertToTspans,t),x&&(_=(-l.lineCount(b)+g)*m*a-_),b.attr("transform",f(e.p[0],e.p[1])+u(e.angle)+f(0,_)).attr("text-anchor","middle").call(i.font,h.title.font)})),b.exit().remove()}},{"../../components/drawing":388,"../../constants/alignment":471,"../../lib":503,"../../lib/svg_text_utils":529,"./makepath":709,"./map_1d_array":710,"./orient_text":711,"@plotly/d3":58}],713:[function(t,e,r){"use strict";var n=t("./constants"),i=t("../../lib/search").findBin,a=t("./compute_control_points"),o=t("./create_spline_evaluator"),s=t("./create_i_derivative_evaluator"),l=t("./create_j_derivative_evaluator");e.exports=function(t){var e=t._a,r=t._b,c=e.length,u=r.length,f=t.aaxis,h=t.baxis,p=e[0],d=e[c-1],m=r[0],g=r[u-1],v=e[e.length-1]-e[0],y=r[r.length-1]-r[0],x=v*n.RELATIVE_CULL_TOLERANCE,b=y*n.RELATIVE_CULL_TOLERANCE;p-=x,d+=x,m-=b,g+=b,t.isVisible=function(t,e){return t>p&&tm&&ed||eg},t.setScale=function(){var e=t._x,r=t._y,n=a(t._xctrl,t._yctrl,e,r,f.smoothing,h.smoothing);t._xctrl=n[0],t._yctrl=n[1],t.evalxy=o([t._xctrl,t._yctrl],c,u,f.smoothing,h.smoothing),t.dxydi=s([t._xctrl,t._yctrl],f.smoothing,h.smoothing),t.dxydj=l([t._xctrl,t._yctrl],f.smoothing,h.smoothing)},t.i2a=function(t){var r=Math.max(0,Math.floor(t[0]),c-2),n=t[0]-r;return(1-n)*e[r]+n*e[r+1]},t.j2b=function(t){var e=Math.max(0,Math.floor(t[1]),c-2),n=t[1]-e;return(1-n)*r[e]+n*r[e+1]},t.ij2ab=function(e){return[t.i2a(e[0]),t.j2b(e[1])]},t.a2i=function(t){var r=Math.max(0,Math.min(i(t,e),c-2)),n=e[r],a=e[r+1];return Math.max(0,Math.min(c-1,r+(t-n)/(a-n)))},t.b2j=function(t){var e=Math.max(0,Math.min(i(t,r),u-2)),n=r[e],a=r[e+1];return Math.max(0,Math.min(u-1,e+(t-n)/(a-n)))},t.ab2ij=function(e){return[t.a2i(e[0]),t.b2j(e[1])]},t.i2c=function(e,r){return t.evalxy([],e,r)},t.ab2xy=function(n,i,a){if(!a&&(ne[c-1]|ir[u-1]))return[!1,!1];var o=t.a2i(n),s=t.b2j(i),l=t.evalxy([],o,s);if(a){var f,h,p,d,m=0,g=0,v=[];ne[c-1]?(f=c-2,h=1,m=(n-e[c-1])/(e[c-1]-e[c-2])):h=o-(f=Math.max(0,Math.min(c-2,Math.floor(o)))),ir[u-1]?(p=u-2,d=1,g=(i-r[u-1])/(r[u-1]-r[u-2])):d=s-(p=Math.max(0,Math.min(u-2,Math.floor(s)))),m&&(t.dxydi(v,f,p,h,d),l[0]+=v[0]*m,l[1]+=v[1]*m),g&&(t.dxydj(v,f,p,h,d),l[0]+=v[0]*g,l[1]+=v[1]*g)}return l},t.c2p=function(t,e,r){return[e.c2p(t[0]),r.c2p(t[1])]},t.p2x=function(t,e,r){return[e.p2c(t[0]),r.p2c(t[1])]},t.dadi=function(t){var r=Math.max(0,Math.min(e.length-2,t));return e[r+1]-e[r]},t.dbdj=function(t){var e=Math.max(0,Math.min(r.length-2,t));return r[e+1]-r[e]},t.dxyda=function(e,r,n,i){var a=t.dxydi(null,e,r,n,i),o=t.dadi(e,n);return[a[0]/o,a[1]/o]},t.dxydb=function(e,r,n,i){var a=t.dxydj(null,e,r,n,i),o=t.dbdj(r,i);return[a[0]/o,a[1]/o]},t.dxyda_rough=function(e,r,n){var i=v*(n||.1),a=t.ab2xy(e+i,r,!0),o=t.ab2xy(e-i,r,!0);return[.5*(a[0]-o[0])/i,.5*(a[1]-o[1])/i]},t.dxydb_rough=function(e,r,n){var i=y*(n||.1),a=t.ab2xy(e,r+i,!0),o=t.ab2xy(e,r-i,!0);return[.5*(a[0]-o[0])/i,.5*(a[1]-o[1])/i]},t.dpdx=function(t){return t._m},t.dpdy=function(t){return t._m}}},{"../../lib/search":523,"./compute_control_points":701,"./constants":702,"./create_i_derivative_evaluator":703,"./create_j_derivative_evaluator":704,"./create_spline_evaluator":705}],714:[function(t,e,r){"use strict";var n=t("../../lib");e.exports=function(t,e,r){var i,a,o,s=[],l=[],c=t[0].length,u=t.length;function f(e,r){var n,i=0,a=0;return e>0&&void 0!==(n=t[r][e-1])&&(a++,i+=n),e0&&void 0!==(n=t[r-1][e])&&(a++,i+=n),r0&&a0&&i1e-5);return n.log("Smoother converged to",k,"after",A,"iterations"),t}},{"../../lib":503}],715:[function(t,e,r){"use strict";var n=t("../../lib").isArray1D;e.exports=function(t,e,r){var i=r("x"),a=i&&i.length,o=r("y"),s=o&&o.length;if(!a&&!s)return!1;if(e._cheater=!i,a&&!n(i)||s&&!n(o))e._length=null;else{var l=a?i.length:1/0;s&&(l=Math.min(l,o.length)),e.a&&e.a.length&&(l=Math.min(l,e.a.length)),e.b&&e.b.length&&(l=Math.min(l,e.b.length)),e._length=l}return!0}},{"../../lib":503}],716:[function(t,e,r){"use strict";var n=t("../../plots/template_attributes").hovertemplateAttrs,i=t("../scattergeo/attributes"),a=t("../../components/colorscale/attributes"),o=t("../../plots/attributes"),s=t("../../components/color/attributes").defaultLine,l=t("../../lib/extend").extendFlat,c=i.marker.line;e.exports=l({locations:{valType:"data_array",editType:"calc"},locationmode:i.locationmode,z:{valType:"data_array",editType:"calc"},geojson:l({},i.geojson,{}),featureidkey:i.featureidkey,text:l({},i.text,{}),hovertext:l({},i.hovertext,{}),marker:{line:{color:l({},c.color,{dflt:s}),width:l({},c.width,{dflt:1}),editType:"calc"},opacity:{valType:"number",arrayOk:!0,min:0,max:1,dflt:1,editType:"style"},editType:"calc"},selected:{marker:{opacity:i.selected.marker.opacity,editType:"plot"},editType:"plot"},unselected:{marker:{opacity:i.unselected.marker.opacity,editType:"plot"},editType:"plot"},hoverinfo:l({},o.hoverinfo,{editType:"calc",flags:["location","z","text","name"]}),hovertemplate:n(),showlegend:l({},o.showlegend,{dflt:!1})},a("",{cLetter:"z",editTypeOverride:"calc"}))},{"../../components/color/attributes":365,"../../components/colorscale/attributes":373,"../../lib/extend":493,"../../plots/attributes":550,"../../plots/template_attributes":633,"../scattergeo/attributes":969}],717:[function(t,e,r){"use strict";var n=t("fast-isnumeric"),i=t("../../constants/numerical").BADNUM,a=t("../../components/colorscale/calc"),o=t("../scatter/arrays_to_calcdata"),s=t("../scatter/calc_selection");function l(t){return t&&"string"==typeof t}e.exports=function(t,e){var r,c=e._length,u=new Array(c);r=e.geojson?function(t){return l(t)||n(t)}:l;for(var f=0;f")}(t,f,o),[t]}},{"../../lib":503,"../../plots/cartesian/axes":554,"./attributes":716}],721:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),supplyDefaults:t("./defaults"),colorbar:t("../heatmap/colorbar"),calc:t("./calc"),calcGeoJSON:t("./plot").calcGeoJSON,plot:t("./plot").plot,style:t("./style").style,styleOnSelect:t("./style").styleOnSelect,hoverPoints:t("./hover"),eventData:t("./event_data"),selectPoints:t("./select"),moduleType:"trace",name:"choropleth",basePlotModule:t("../../plots/geo"),categories:["geo","noOpacity","showLegend"],meta:{}}},{"../../plots/geo":589,"../heatmap/colorbar":795,"./attributes":716,"./calc":717,"./defaults":718,"./event_data":719,"./hover":720,"./plot":722,"./select":723,"./style":724}],722:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../lib"),a=t("../../lib/geo_location_utils"),o=t("../../lib/topojson_utils").getTopojsonFeatures,s=t("../../plots/cartesian/autorange").findExtremes,l=t("./style").style;e.exports={calcGeoJSON:function(t,e){for(var r=t[0].trace,n=e[r.geo],i=n._subplot,l=r.locationmode,c=r._length,u="geojson-id"===l?a.extractTraceFeature(t):o(r,i.topojson),f=[],h=[],p=0;p=0;n--){var i=r[n].id;if("string"==typeof i&&0===i.indexOf("water"))for(var a=n+1;a=0;r--)t.removeLayer(e[r][1])},s.dispose=function(){var t=this.subplot.map;this._removeLayers(),t.removeSource(this.sourceId)},e.exports=function(t,e){var r=e[0].trace,i=new o(t,r.uid),a=i.sourceId,s=n(e),l=i.below=t.belowLookup["trace-"+r.uid];return t.map.addSource(a,{type:"geojson",data:s.geojson}),i._addLayers(s,l),e[0].trace._glTrace=i,i}},{"../../plots/mapbox/constants":611,"./convert":726}],730:[function(t,e,r){"use strict";var n=t("../../components/colorscale/attributes"),i=t("../../plots/cartesian/axis_format_attributes").axisHoverFormat,a=t("../../plots/template_attributes").hovertemplateAttrs,o=t("../mesh3d/attributes"),s=t("../../plots/attributes"),l=t("../../lib/extend").extendFlat,c={x:{valType:"data_array",editType:"calc+clearAxisTypes"},y:{valType:"data_array",editType:"calc+clearAxisTypes"},z:{valType:"data_array",editType:"calc+clearAxisTypes"},u:{valType:"data_array",editType:"calc"},v:{valType:"data_array",editType:"calc"},w:{valType:"data_array",editType:"calc"},sizemode:{valType:"enumerated",values:["scaled","absolute"],editType:"calc",dflt:"scaled"},sizeref:{valType:"number",editType:"calc",min:0},anchor:{valType:"enumerated",editType:"calc",values:["tip","tail","cm","center"],dflt:"cm"},text:{valType:"string",dflt:"",arrayOk:!0,editType:"calc"},hovertext:{valType:"string",dflt:"",arrayOk:!0,editType:"calc"},hovertemplate:a({editType:"calc"},{keys:["norm"]}),uhoverformat:i("u",1),vhoverformat:i("v",1),whoverformat:i("w",1),xhoverformat:i("x"),yhoverformat:i("y"),zhoverformat:i("z"),showlegend:l({},s.showlegend,{dflt:!1})};l(c,n("",{colorAttr:"u/v/w norm",showScaleDflt:!0,editTypeOverride:"calc"}));["opacity","lightposition","lighting"].forEach((function(t){c[t]=o[t]})),c.hoverinfo=l({},s.hoverinfo,{editType:"calc",flags:["x","y","z","u","v","w","norm","text","name"],dflt:"x+y+z+norm+text+name"}),c.transforms=void 0,e.exports=c},{"../../components/colorscale/attributes":373,"../../lib/extend":493,"../../plots/attributes":550,"../../plots/cartesian/axis_format_attributes":557,"../../plots/template_attributes":633,"../mesh3d/attributes":867}],731:[function(t,e,r){"use strict";var n=t("../../components/colorscale/calc");e.exports=function(t,e){for(var r=e.u,i=e.v,a=e.w,o=Math.min(e.x.length,e.y.length,e.z.length,r.length,i.length,a.length),s=-1/0,l=1/0,c=0;co.level||o.starts.length&&a===o.level)}break;case"constraint":if(n.prefixBoundary=!1,n.edgepaths.length)return;var s=n.x.length,l=n.y.length,c=-1/0,u=1/0;for(r=0;r":p>c&&(n.prefixBoundary=!0);break;case"<":(pc||n.starts.length&&h===u)&&(n.prefixBoundary=!0);break;case"][":f=Math.min(p[0],p[1]),h=Math.max(p[0],p[1]),fc&&(n.prefixBoundary=!0)}}}},{}],738:[function(t,e,r){"use strict";var n=t("../../components/colorscale"),i=t("./make_color_map"),a=t("./end_plus");e.exports={min:"zmin",max:"zmax",calc:function(t,e,r){var o=e.contours,s=e.line,l=o.size||1,c=o.coloring,u=i(e,{isColorbar:!0});if("heatmap"===c){var f=n.extractOpts(e);r._fillgradient=f.reversescale?n.flipScale(f.colorscale):f.colorscale,r._zrange=[f.min,f.max]}else"fill"===c&&(r._fillcolor=u);r._line={color:"lines"===c?u:s.color,width:!1!==o.showlines?s.width:0,dash:s.dash},r._levels={start:o.start,end:a(o),size:l}}}},{"../../components/colorscale":378,"./end_plus":746,"./make_color_map":751}],739:[function(t,e,r){"use strict";e.exports={BOTTOMSTART:[1,9,13,104,713],TOPSTART:[4,6,7,104,713],LEFTSTART:[8,12,14,208,1114],RIGHTSTART:[2,3,11,208,1114],NEWDELTA:[null,[-1,0],[0,-1],[-1,0],[1,0],null,[0,-1],[-1,0],[0,1],[0,1],null,[0,1],[1,0],[1,0],[0,-1]],CHOOSESADDLE:{104:[4,1],208:[2,8],713:[7,13],1114:[11,14]},SADDLEREMAINDER:{1:4,2:8,4:1,7:13,8:2,11:14,13:7,14:11},LABELDISTANCE:2,LABELINCREASE:10,LABELMIN:3,LABELMAX:10,LABELOPTIMIZER:{EDGECOST:1,ANGLECOST:1,NEIGHBORCOST:5,SAMELEVELFACTOR:10,SAMELEVELDISTANCE:5,MAXCOST:100,INITIALSEARCHPOINTS:10,ITERATIONS:5}}},{}],740:[function(t,e,r){"use strict";var n=t("fast-isnumeric"),i=t("./label_defaults"),a=t("../../components/color"),o=a.addOpacity,s=a.opacity,l=t("../../constants/filter_ops"),c=l.CONSTRAINT_REDUCTION,u=l.COMPARISON_OPS2;e.exports=function(t,e,r,a,l,f){var h,p,d,m=e.contours,g=r("contours.operation");(m._operation=c[g],function(t,e){var r;-1===u.indexOf(e.operation)?(t("contours.value",[0,1]),Array.isArray(e.value)?e.value.length>2?e.value=e.value.slice(2):0===e.length?e.value=[0,1]:e.length<2?(r=parseFloat(e.value[0]),e.value=[r,r+1]):e.value=[parseFloat(e.value[0]),parseFloat(e.value[1])]:n(e.value)&&(r=parseFloat(e.value),e.value=[r,r+1])):(t("contours.value",0),n(e.value)||(Array.isArray(e.value)?e.value=parseFloat(e.value[0]):e.value=0))}(r,m),"="===g?h=m.showlines=!0:(h=r("contours.showlines"),d=r("fillcolor",o((t.line||{}).color||l,.5))),h)&&(p=r("line.color",d&&s(d)?o(e.fillcolor,1):l),r("line.width",2),r("line.dash"));r("line.smoothing"),i(r,a,p,f)}},{"../../components/color":366,"../../constants/filter_ops":475,"./label_defaults":750,"fast-isnumeric":190}],741:[function(t,e,r){"use strict";var n=t("../../constants/filter_ops"),i=t("fast-isnumeric");function a(t,e){var r,a=Array.isArray(e);function o(t){return i(t)?+t:null}return-1!==n.COMPARISON_OPS2.indexOf(t)?r=o(a?e[0]:e):-1!==n.INTERVAL_OPS.indexOf(t)?r=a?[o(e[0]),o(e[1])]:[o(e),o(e)]:-1!==n.SET_OPS.indexOf(t)&&(r=a?e.map(o):[o(e)]),r}function o(t){return function(e){e=a(t,e);var r=Math.min(e[0],e[1]),n=Math.max(e[0],e[1]);return{start:r,end:n,size:n-r}}}function s(t){return function(e){return{start:e=a(t,e),end:1/0,size:1/0}}}e.exports={"[]":o("[]"),"][":o("]["),">":s(">"),"<":s("<"),"=":s("=")}},{"../../constants/filter_ops":475,"fast-isnumeric":190}],742:[function(t,e,r){"use strict";e.exports=function(t,e,r,n){var i=n("contours.start"),a=n("contours.end"),o=!1===i||!1===a,s=r("contours.size");!(o?e.autocontour=!0:r("autocontour",!1))&&s||r("ncontours")}},{}],743:[function(t,e,r){"use strict";var n=t("../../lib");function i(t){return n.extendFlat({},t,{edgepaths:n.extendDeep([],t.edgepaths),paths:n.extendDeep([],t.paths),starts:n.extendDeep([],t.starts)})}e.exports=function(t,e){var r,a,o,s=function(t){return t.reverse()},l=function(t){return t};switch(e){case"=":case"<":return t;case">":for(1!==t.length&&n.warn("Contour data invalid for the specified inequality operation."),a=t[0],r=0;r1e3){n.warn("Too many contours, clipping at 1000",t);break}return l}},{"../../lib":503,"./constraint_mapping":741,"./end_plus":746}],746:[function(t,e,r){"use strict";e.exports=function(t){return t.end+t.size/1e6}},{}],747:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("./constants");function a(t,e,r,n){return Math.abs(t[0]-e[0])20&&e?208===t||1114===t?n=0===r[0]?1:-1:a=0===r[1]?1:-1:-1!==i.BOTTOMSTART.indexOf(t)?a=1:-1!==i.LEFTSTART.indexOf(t)?n=1:-1!==i.TOPSTART.indexOf(t)?a=-1:n=-1;return[n,a]}(f,r,e),p=[s(t,e,[-h[0],-h[1]])],d=t.z.length,m=t.z[0].length,g=e.slice(),v=h.slice();for(c=0;c<1e4;c++){if(f>20?(f=i.CHOOSESADDLE[f][(h[0]||h[1])<0?0:1],t.crossings[u]=i.SADDLEREMAINDER[f]):delete t.crossings[u],!(h=i.NEWDELTA[f])){n.log("Found bad marching index:",f,e,t.level);break}p.push(s(t,e,h)),e[0]+=h[0],e[1]+=h[1],u=e.join(","),a(p[p.length-1],p[p.length-2],o,l)&&p.pop();var y=h[0]&&(e[0]<0||e[0]>m-2)||h[1]&&(e[1]<0||e[1]>d-2);if(e[0]===g[0]&&e[1]===g[1]&&h[0]===v[0]&&h[1]===v[1]||r&&y)break;f=t.crossings[u]}1e4===c&&n.log("Infinite loop in contour?");var x,b,_,w,T,k,A,M,S,E,L,C,P,I,O,z=a(p[0],p[p.length-1],o,l),D=0,R=.2*t.smoothing,F=[],B=0;for(c=1;c=B;c--)if((x=F[c])=B&&x+F[b]M&&S--,t.edgepaths[S]=L.concat(p,E));break}V||(t.edgepaths[M]=p.concat(E))}for(M=0;Mt?0:1)+(e[0][1]>t?0:2)+(e[1][1]>t?0:4)+(e[1][0]>t?0:8);return 5===r||10===r?t>(e[0][0]+e[0][1]+e[1][0]+e[1][1])/4?5===r?713:1114:5===r?104:208:15===r?0:r}e.exports=function(t){var e,r,a,o,s,l,c,u,f,h=t[0].z,p=h.length,d=h[0].length,m=2===p||2===d;for(r=0;r=0&&(n=y,s=l):Math.abs(r[1]-n[1])<.01?Math.abs(r[1]-y[1])<.01&&(y[0]-r[0])*(n[0]-y[0])>=0&&(n=y,s=l):i.log("endpt to newendpt is not vert. or horz.",r,n,y)}if(r=n,s>=0)break;f+="L"+n}if(s===t.edgepaths.length){i.log("unclosed perimeter path");break}h=s,(d=-1===p.indexOf(h))&&(h=p[0],f+="Z")}for(h=0;hn.center?n.right-s:s-n.left)/(u+Math.abs(Math.sin(c)*o)),p=(l>n.middle?n.bottom-l:l-n.top)/(Math.abs(f)+Math.cos(c)*o);if(h<1||p<1)return 1/0;var d=v.EDGECOST*(1/(h-1)+1/(p-1));d+=v.ANGLECOST*c*c;for(var m=s-u,g=l-f,y=s+u,x=l+f,b=0;b2*v.MAXCOST)break;p&&(s/=2),l=(o=c-s/2)+1.5*s}if(h<=v.MAXCOST)return u},r.addLabelData=function(t,e,r,n){var i=e.fontSize,a=e.width+i/3,o=Math.max(0,e.height-i/3),s=t.x,l=t.y,c=t.theta,u=Math.sin(c),f=Math.cos(c),h=function(t,e){return[s+t*f-e*u,l+t*u+e*f]},p=[h(-a/2,-o/2),h(-a/2,o/2),h(a/2,o/2),h(a/2,-o/2)];r.push({text:e.text,x:s,y:l,dy:e.dy,theta:c,level:e.level,width:a,height:o}),n.push(p)},r.drawLabels=function(t,e,r,a,o){var l=t.selectAll("text").data(e,(function(t){return t.text+","+t.x+","+t.y+","+t.theta}));if(l.exit().remove(),l.enter().append("text").attr({"data-notex":1,"text-anchor":"middle"}).each((function(t){var e=t.x+Math.sin(t.theta)*t.dy,i=t.y-Math.cos(t.theta)*t.dy;n.select(this).text(t.text).attr({x:e,y:i,transform:"rotate("+180*t.theta/Math.PI+" "+e+" "+i+")"}).call(s.convertToTspans,r)})),o){for(var c="",u=0;ur.end&&(r.start=r.end=(r.start+r.end)/2),t._input.contours||(t._input.contours={}),i.extendFlat(t._input.contours,{start:r.start,end:r.end,size:r.size}),t._input.autocontour=!0}else if("constraint"!==r.type){var c,u=r.start,f=r.end,h=t._input.contours;if(u>f&&(r.start=h.start=f,f=r.end=h.end=u,u=r.start),!(r.size>0))c=u===f?1:a(u,f,t.ncontours).dtick,h.size=r.size=c}}},{"../../lib":503,"../../plots/cartesian/axes":554}],755:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../components/drawing"),a=t("../heatmap/style"),o=t("./make_color_map");e.exports=function(t){var e=n.select(t).selectAll("g.contour");e.style("opacity",(function(t){return t[0].trace.opacity})),e.each((function(t){var e=n.select(this),r=t[0].trace,a=r.contours,s=r.line,l=a.size||1,c=a.start,u="constraint"===a.type,f=!u&&"lines"===a.coloring,h=!u&&"fill"===a.coloring,p=f||h?o(r):null;e.selectAll("g.contourlevel").each((function(t){n.select(this).selectAll("path").call(i.lineGroupStyle,s.width,f?p(t.level):s.color,s.dash)}));var d=a.labelfont;if(e.selectAll("g.contourlabels text").each((function(t){i.font(n.select(this),{family:d.family,size:d.size,color:d.color||(f?p(t.level):s.color)})})),u)e.selectAll("g.contourfill path").style("fill",r.fillcolor);else if(h){var m;e.selectAll("g.contourfill path").style("fill",(function(t){return void 0===m&&(m=t.level),p(t.level+.5*l)})),void 0===m&&(m=c),e.selectAll("g.contourbg path").style("fill",p(m-.5*l))}})),a(t)}},{"../../components/drawing":388,"../heatmap/style":805,"./make_color_map":751,"@plotly/d3":58}],756:[function(t,e,r){"use strict";var n=t("../../components/colorscale/defaults"),i=t("./label_defaults");e.exports=function(t,e,r,a,o){var s,l=r("contours.coloring"),c="";"fill"===l&&(s=r("contours.showlines")),!1!==s&&("lines"!==l&&(c=r("line.color","#000")),r("line.width",.5),r("line.dash")),"none"!==l&&(!0!==t.showlegend&&(e.showlegend=!1),e._dfltShowLegend=!1,n(t,e,a,r,{prefix:"",cLetter:"z"})),r("line.smoothing"),i(r,a,c,o)}},{"../../components/colorscale/defaults":376,"./label_defaults":750}],757:[function(t,e,r){"use strict";var n=t("../heatmap/attributes"),i=t("../contour/attributes"),a=t("../../components/colorscale/attributes"),o=t("../../lib/extend").extendFlat,s=i.contours;e.exports=o({carpet:{valType:"string",editType:"calc"},z:n.z,a:n.x,a0:n.x0,da:n.dx,b:n.y,b0:n.y0,db:n.dy,text:n.text,hovertext:n.hovertext,transpose:n.transpose,atype:n.xtype,btype:n.ytype,fillcolor:i.fillcolor,autocontour:i.autocontour,ncontours:i.ncontours,contours:{type:s.type,start:s.start,end:s.end,size:s.size,coloring:{valType:"enumerated",values:["fill","lines","none"],dflt:"fill",editType:"calc"},showlines:s.showlines,showlabels:s.showlabels,labelfont:s.labelfont,labelformat:s.labelformat,operation:s.operation,value:s.value,editType:"calc",impliedEdits:{autocontour:!1}},line:{color:i.line.color,width:i.line.width,dash:i.line.dash,smoothing:i.line.smoothing,editType:"plot"},transforms:void 0},a("",{cLetter:"z",autoColorDflt:!1}))},{"../../components/colorscale/attributes":373,"../../lib/extend":493,"../contour/attributes":735,"../heatmap/attributes":792}],758:[function(t,e,r){"use strict";var n=t("../../components/colorscale/calc"),i=t("../../lib"),a=t("../heatmap/convert_column_xyz"),o=t("../heatmap/clean_2d_array"),s=t("../heatmap/interp2d"),l=t("../heatmap/find_empties"),c=t("../heatmap/make_bound_array"),u=t("./defaults"),f=t("../carpet/lookup_carpetid"),h=t("../contour/set_contours");e.exports=function(t,e){var r=e._carpetTrace=f(t,e);if(r&&r.visible&&"legendonly"!==r.visible){if(!e.a||!e.b){var p=t.data[r.index],d=t.data[e.index];d.a||(d.a=p.a),d.b||(d.b=p.b),u(d,e,e._defaultColor,t._fullLayout)}var m=function(t,e){var r,u,f,h,p,d,m,g=e._carpetTrace,v=g.aaxis,y=g.baxis;v._minDtick=0,y._minDtick=0,i.isArray1D(e.z)&&a(e,v,y,"a","b",["z"]);r=e._a=e._a||e.a,h=e._b=e._b||e.b,r=r?v.makeCalcdata(e,"_a"):[],h=h?y.makeCalcdata(e,"_b"):[],u=e.a0||0,f=e.da||1,p=e.b0||0,d=e.db||1,m=e._z=o(e._z||e.z,e.transpose),e._emptypoints=l(m),s(m,e._emptypoints);var x=i.maxRowLength(m),b="scaled"===e.xtype?"":r,_=c(e,b,u,f,x,v),w="scaled"===e.ytype?"":h,T=c(e,w,p,d,m.length,y),k={a:_,b:T,z:m};"levels"===e.contours.type&&"none"!==e.contours.coloring&&n(t,e,{vals:m,containerStr:"",cLetter:"z"});return[k]}(t,e);return h(e,e._z),m}}},{"../../components/colorscale/calc":374,"../../lib":503,"../carpet/lookup_carpetid":708,"../contour/set_contours":754,"../heatmap/clean_2d_array":794,"../heatmap/convert_column_xyz":796,"../heatmap/find_empties":798,"../heatmap/interp2d":801,"../heatmap/make_bound_array":803,"./defaults":759}],759:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("../heatmap/xyz_defaults"),a=t("./attributes"),o=t("../contour/constraint_defaults"),s=t("../contour/contours_defaults"),l=t("../contour/style_defaults");e.exports=function(t,e,r,c){function u(r,i){return n.coerce(t,e,a,r,i)}if(u("carpet"),t.a&&t.b){if(!i(t,e,u,c,"a","b"))return void(e.visible=!1);u("text"),"constraint"===u("contours.type")?o(t,e,u,c,r,{hasHover:!1}):(s(t,e,u,(function(r){return n.coerce2(t,e,a,r)})),l(t,e,u,c,{hasHover:!1}))}else e._defaultColor=r,e._length=null}},{"../../lib":503,"../contour/constraint_defaults":740,"../contour/contours_defaults":742,"../contour/style_defaults":756,"../heatmap/xyz_defaults":807,"./attributes":757}],760:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),supplyDefaults:t("./defaults"),colorbar:t("../contour/colorbar"),calc:t("./calc"),plot:t("./plot"),style:t("../contour/style"),moduleType:"trace",name:"contourcarpet",basePlotModule:t("../../plots/cartesian"),categories:["cartesian","svg","carpet","contour","symbols","showLegend","hasLines","carpetDependent","noHover","noSortingByValue"],meta:{}}},{"../../plots/cartesian":568,"../contour/colorbar":738,"../contour/style":755,"./attributes":757,"./calc":758,"./defaults":759,"./plot":761}],761:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../carpet/map_1d_array"),a=t("../carpet/makepath"),o=t("../../components/drawing"),s=t("../../lib"),l=t("../contour/make_crossings"),c=t("../contour/find_all_paths"),u=t("../contour/plot"),f=t("../contour/constants"),h=t("../contour/convert_to_constraints"),p=t("../contour/empty_pathinfo"),d=t("../contour/close_boundaries"),m=t("../carpet/lookup_carpetid"),g=t("../carpet/axis_aligned_line");function v(t,e,r){var n=t.getPointAtLength(e),i=t.getPointAtLength(r),a=i.x-n.x,o=i.y-n.y,s=Math.sqrt(a*a+o*o);return[a/s,o/s]}function y(t){var e=Math.sqrt(t[0]*t[0]+t[1]*t[1]);return[t[0]/e,t[1]/e]}function x(t,e){var r=Math.abs(t[0]*e[0]+t[1]*e[1]);return Math.sqrt(1-r*r)/r}e.exports=function(t,e,r,b){var _=e.xaxis,w=e.yaxis;s.makeTraceGroups(b,r,"contour").each((function(r){var b=n.select(this),T=r[0],k=T.trace,A=k._carpetTrace=m(t,k),M=t.calcdata[A.index][0];if(A.visible&&"legendonly"!==A.visible){var S=T.a,E=T.b,L=k.contours,C=p(L,e,T),P="constraint"===L.type,I=L._operation,O=P?"="===I?"lines":"fill":L.coloring,z=[[S[0],E[E.length-1]],[S[S.length-1],E[E.length-1]],[S[S.length-1],E[0]],[S[0],E[0]]];l(C);var D=1e-8*(S[S.length-1]-S[0]),R=1e-8*(E[E.length-1]-E[0]);c(C,D,R);var F,B,N,j,U=C;"constraint"===L.type&&(U=h(C,I)),function(t,e){var r,n,i,a,o,s,l,c,u;for(r=0;r=0;j--)F=M.clipsegments[j],B=i([],F.x,_.c2p),N=i([],F.y,w.c2p),B.reverse(),N.reverse(),V.push(a(B,N,F.bicubic));var H="M"+V.join("L")+"Z";!function(t,e,r,n,o,l){var c,u,f,h,p=s.ensureSingle(t,"g","contourbg").selectAll("path").data("fill"!==l||o?[]:[0]);p.enter().append("path"),p.exit().remove();var d=[];for(h=0;h=0&&(h=L,d=m):Math.abs(f[1]-h[1])=0&&(h=L,d=m):s.log("endpt to newendpt is not vert. or horz.",f,h,L)}if(d>=0)break;y+=S(f,h),f=h}if(d===e.edgepaths.length){s.log("unclosed perimeter path");break}u=d,(b=-1===x.indexOf(u))&&(u=x[0],y+=S(f,h)+"Z",f=null)}for(u=0;ug&&(n.max=g);n.len=n.max-n.min}(this,r,t,n,c,e.height),!(n.len<(e.width+e.height)*f.LABELMIN)))for(var i=Math.min(Math.ceil(n.len/I),f.LABELMAX),a=0;a0?+p[u]:0),f.push({type:"Feature",geometry:{type:"Point",coordinates:v},properties:y})}}var b=o.extractOpts(e),_=b.reversescale?o.flipScale(b.colorscale):b.colorscale,w=_[0][1],T=["interpolate",["linear"],["heatmap-density"],0,a.opacity(w)<1?w:a.addOpacity(w,0)];for(u=1;u<_.length;u++)T.push(_[u][0],_[u][1]);var k=["interpolate",["linear"],["get","z"],b.min,0,b.max,1];return i.extendFlat(c.heatmap.paint,{"heatmap-weight":d?k:1/(b.max-b.min),"heatmap-color":T,"heatmap-radius":m?{type:"identity",property:"r"}:e.radius,"heatmap-opacity":e.opacity}),c.geojson={type:"FeatureCollection",features:f},c.heatmap.layout.visibility="visible",c}},{"../../components/color":366,"../../components/colorscale":378,"../../constants/numerical":479,"../../lib":503,"../../lib/geojson_utils":497,"fast-isnumeric":190}],765:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("../../components/colorscale/defaults"),a=t("./attributes");e.exports=function(t,e,r,o){function s(r,i){return n.coerce(t,e,a,r,i)}var l=s("lon")||[],c=s("lat")||[],u=Math.min(l.length,c.length);u?(e._length=u,s("z"),s("radius"),s("below"),s("text"),s("hovertext"),s("hovertemplate"),i(t,e,o,s,{prefix:"",cLetter:"z"})):e.visible=!1}},{"../../components/colorscale/defaults":376,"../../lib":503,"./attributes":762}],766:[function(t,e,r){"use strict";e.exports=function(t,e){return t.lon=e.lon,t.lat=e.lat,t.z=e.z,t}},{}],767:[function(t,e,r){"use strict";var n=t("../../plots/cartesian/axes"),i=t("../scattermapbox/hover").hoverPoints,a=t("../scattermapbox/hover").getExtraText;e.exports=function(t,e,r){var o=i(t,e,r);if(o){var s=o[0],l=s.cd,c=l[0].trace,u=l[s.index];if(delete s.color,"z"in u){var f=s.subplot.mockAxis;s.z=u.z,s.zLabel=n.tickText(f,f.c2l(u.z),"hover").text}return s.extraText=a(c,u,l[0].t.labels),[s]}}},{"../../plots/cartesian/axes":554,"../scattermapbox/hover":998}],768:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),supplyDefaults:t("./defaults"),colorbar:t("../heatmap/colorbar"),formatLabels:t("../scattermapbox/format_labels"),calc:t("./calc"),plot:t("./plot"),hoverPoints:t("./hover"),eventData:t("./event_data"),getBelow:function(t,e){for(var r=e.getMapLayers(),n=0;n=0;r--)t.removeLayer(e[r][1])},o.dispose=function(){var t=this.subplot.map;this._removeLayers(),t.removeSource(this.sourceId)},e.exports=function(t,e){var r=e[0].trace,i=new a(t,r.uid),o=i.sourceId,s=n(e),l=i.below=t.belowLookup["trace-"+r.uid];return t.map.addSource(o,{type:"geojson",data:s.geojson}),i._addLayers(s,l),i}},{"../../plots/mapbox/constants":611,"./convert":764}],770:[function(t,e,r){"use strict";var n=t("../../lib");e.exports=function(t,e){for(var r=0;r"),l.color=function(t,e){var r=t.marker,i=e.mc||r.color,a=e.mlc||r.line.color,o=e.mlw||r.line.width;if(n(i))return i;if(n(a)&&o)return a}(u,h),[l]}}},{"../../components/color":366,"../../lib":503,"../bar/hover":655}],778:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),layoutAttributes:t("./layout_attributes"),supplyDefaults:t("./defaults").supplyDefaults,crossTraceDefaults:t("./defaults").crossTraceDefaults,supplyLayoutDefaults:t("./layout_defaults"),calc:t("./calc"),crossTraceCalc:t("./cross_trace_calc"),plot:t("./plot"),style:t("./style").style,hoverPoints:t("./hover"),eventData:t("./event_data"),selectPoints:t("../bar/select"),moduleType:"trace",name:"funnel",basePlotModule:t("../../plots/cartesian"),categories:["bar-like","cartesian","svg","oriented","showLegend","zoomScale"],meta:{}}},{"../../plots/cartesian":568,"../bar/select":660,"./attributes":771,"./calc":772,"./cross_trace_calc":774,"./defaults":775,"./event_data":776,"./hover":777,"./layout_attributes":779,"./layout_defaults":780,"./plot":781,"./style":782}],779:[function(t,e,r){"use strict";e.exports={funnelmode:{valType:"enumerated",values:["stack","group","overlay"],dflt:"stack",editType:"calc"},funnelgap:{valType:"number",min:0,max:1,editType:"calc"},funnelgroupgap:{valType:"number",min:0,max:1,dflt:0,editType:"calc"}}},{}],780:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("./layout_attributes");e.exports=function(t,e,r){var a=!1;function o(r,a){return n.coerce(t,e,i,r,a)}for(var s=0;s path").each((function(t){if(!t.isBlank){var e=s.marker;n.select(this).call(a.fill,t.mc||e.color).call(a.stroke,t.mlc||e.line.color).call(i.dashLine,e.line.dash,t.mlw||e.line.width).style("opacity",s.selectedpoints&&!t.selected?o:1)}})),c(r,s,t),r.selectAll(".regions").each((function(){n.select(this).selectAll("path").style("stroke-width",0).call(a.fill,s.connector.fillcolor)})),r.selectAll(".lines").each((function(){var t=s.connector.line;i.lineGroupStyle(n.select(this).selectAll("path"),t.width,t.color,t.dash)}))}))}}},{"../../components/color":366,"../../components/drawing":388,"../../constants/interactions":478,"../bar/style":662,"../bar/uniform_text":664,"@plotly/d3":58}],783:[function(t,e,r){"use strict";var n=t("../pie/attributes"),i=t("../../plots/attributes"),a=t("../../plots/domain").attributes,o=t("../../plots/template_attributes").hovertemplateAttrs,s=t("../../plots/template_attributes").texttemplateAttrs,l=t("../../lib/extend").extendFlat;e.exports={labels:n.labels,label0:n.label0,dlabel:n.dlabel,values:n.values,marker:{colors:n.marker.colors,line:{color:l({},n.marker.line.color,{dflt:null}),width:l({},n.marker.line.width,{dflt:1}),editType:"calc"},editType:"calc"},text:n.text,hovertext:n.hovertext,scalegroup:l({},n.scalegroup,{}),textinfo:l({},n.textinfo,{flags:["label","text","value","percent"]}),texttemplate:s({editType:"plot"},{keys:["label","color","value","text","percent"]}),hoverinfo:l({},i.hoverinfo,{flags:["label","text","value","percent","name"]}),hovertemplate:o({},{keys:["label","color","value","text","percent"]}),textposition:l({},n.textposition,{values:["inside","none"],dflt:"inside"}),textfont:n.textfont,insidetextfont:n.insidetextfont,title:{text:n.title.text,font:n.title.font,position:l({},n.title.position,{values:["top left","top center","top right"],dflt:"top center"}),editType:"plot"},domain:a({name:"funnelarea",trace:!0,editType:"calc"}),aspectratio:{valType:"number",min:0,dflt:1,editType:"plot"},baseratio:{valType:"number",min:0,max:1,dflt:.333,editType:"plot"}}},{"../../lib/extend":493,"../../plots/attributes":550,"../../plots/domain":584,"../../plots/template_attributes":633,"../pie/attributes":901}],784:[function(t,e,r){"use strict";var n=t("../../plots/plots");r.name="funnelarea",r.plot=function(t,e,i,a){n.plotBasePlot(r.name,t,e,i,a)},r.clean=function(t,e,i,a){n.cleanBasePlot(r.name,t,e,i,a)}},{"../../plots/plots":619}],785:[function(t,e,r){"use strict";var n=t("../pie/calc");e.exports={calc:function(t,e){return n.calc(t,e)},crossTraceCalc:function(t){n.crossTraceCalc(t,{type:"funnelarea"})}}},{"../pie/calc":903}],786:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("./attributes"),a=t("../../plots/domain").defaults,o=t("../bar/defaults").handleText,s=t("../pie/defaults").handleLabelsAndValues;e.exports=function(t,e,r,l){function c(r,a){return n.coerce(t,e,i,r,a)}var u=c("labels"),f=c("values"),h=s(u,f),p=h.len;if(e._hasLabels=h.hasLabels,e._hasValues=h.hasValues,!e._hasLabels&&e._hasValues&&(c("label0"),c("dlabel")),p){e._length=p,c("marker.line.width")&&c("marker.line.color",l.paper_bgcolor),c("marker.colors"),c("scalegroup");var d,m=c("text"),g=c("texttemplate");if(g||(d=c("textinfo",Array.isArray(m)?"text+percent":"percent")),c("hovertext"),c("hovertemplate"),g||d&&"none"!==d){var v=c("textposition");o(t,e,l,c,v,{moduleHasSelected:!1,moduleHasUnselected:!1,moduleHasConstrain:!1,moduleHasCliponaxis:!1,moduleHasTextangle:!1,moduleHasInsideanchor:!1})}a(e,l,c),c("title.text")&&(c("title.position"),n.coerceFont(c,"title.font",l.font)),c("aspectratio"),c("baseratio")}else e.visible=!1}},{"../../lib":503,"../../plots/domain":584,"../bar/defaults":652,"../pie/defaults":904,"./attributes":783}],787:[function(t,e,r){"use strict";e.exports={moduleType:"trace",name:"funnelarea",basePlotModule:t("./base_plot"),categories:["pie-like","funnelarea","showLegend"],attributes:t("./attributes"),layoutAttributes:t("./layout_attributes"),supplyDefaults:t("./defaults"),supplyLayoutDefaults:t("./layout_defaults"),calc:t("./calc").calc,crossTraceCalc:t("./calc").crossTraceCalc,plot:t("./plot"),style:t("./style"),styleOne:t("../pie/style_one"),meta:{}}},{"../pie/style_one":912,"./attributes":783,"./base_plot":784,"./calc":785,"./defaults":786,"./layout_attributes":788,"./layout_defaults":789,"./plot":790,"./style":791}],788:[function(t,e,r){"use strict";var n=t("../pie/layout_attributes").hiddenlabels;e.exports={hiddenlabels:n,funnelareacolorway:{valType:"colorlist",editType:"calc"},extendfunnelareacolors:{valType:"boolean",dflt:!0,editType:"calc"}}},{"../pie/layout_attributes":908}],789:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("./layout_attributes");e.exports=function(t,e){function r(r,a){return n.coerce(t,e,i,r,a)}r("hiddenlabels"),r("funnelareacolorway",e.colorway),r("extendfunnelareacolors")}},{"../../lib":503,"./layout_attributes":788}],790:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../components/drawing"),a=t("../../lib"),o=a.strScale,s=a.strTranslate,l=t("../../lib/svg_text_utils"),c=t("../bar/plot").toMoveInsideBar,u=t("../bar/uniform_text"),f=u.recordMinTextSize,h=u.clearMinTextSize,p=t("../pie/helpers"),d=t("../pie/plot"),m=d.attachFxHandlers,g=d.determineInsideTextFont,v=d.layoutAreas,y=d.prerenderTitles,x=d.positionTitleOutside,b=d.formatSliceLabel;function _(t,e){return"l"+(e[0]-t[0])+","+(e[1]-t[1])}e.exports=function(t,e){var r=t._fullLayout;h("funnelarea",r),y(e,t),v(e,r._size),a.makeTraceGroups(r._funnelarealayer,e,"trace").each((function(e){var u=n.select(this),h=e[0],d=h.trace;!function(t){if(!t.length)return;var e=t[0],r=e.trace,n=r.aspectratio,i=r.baseratio;i>.999&&(i=.999);var a,o=Math.pow(i,2),s=e.vTotal,l=s,c=s*o/(1-o)/s;function u(){var t,e={x:t=Math.sqrt(c),y:-t};return[e.x,e.y]}var f,h,p=[];for(p.push(u()),f=t.length-1;f>-1;f--)if(!(h=t[f]).hidden){var d=h.v/l;c+=d,p.push(u())}var m=1/0,g=-1/0;for(f=0;f-1;f--)if(!(h=t[f]).hidden){var A=p[k+=1][0],M=p[k][1];h.TL=[-A,M],h.TR=[A,M],h.BL=w,h.BR=T,h.pxmid=(S=h.TR,E=h.BR,[.5*(S[0]+E[0]),.5*(S[1]+E[1])]),w=h.TL,T=h.TR}var S,E}(e),u.each((function(){var u=n.select(this).selectAll("g.slice").data(e);u.enter().append("g").classed("slice",!0),u.exit().remove(),u.each((function(o,s){if(o.hidden)n.select(this).selectAll("path,g").remove();else{o.pointNumber=o.i,o.curveNumber=d.index;var u=h.cx,v=h.cy,y=n.select(this),x=y.selectAll("path.surface").data([o]);x.enter().append("path").classed("surface",!0).style({"pointer-events":"all"}),y.call(m,t,e);var w="M"+(u+o.TR[0])+","+(v+o.TR[1])+_(o.TR,o.BR)+_(o.BR,o.BL)+_(o.BL,o.TL)+"Z";x.attr("d",w),b(t,o,h);var T=p.castOption(d.textposition,o.pts),k=y.selectAll("g.slicetext").data(o.text&&"none"!==T?[0]:[]);k.enter().append("g").classed("slicetext",!0),k.exit().remove(),k.each((function(){var h=a.ensureSingle(n.select(this),"text","",(function(t){t.attr("data-notex",1)})),p=a.ensureUniformFontSize(t,g(d,o,r.font));h.text(o.text).attr({class:"slicetext",transform:"","text-anchor":"middle"}).call(i.font,p).call(l.convertToTspans,t);var m,y,x,b=i.bBox(h.node()),_=Math.min(o.BL[1],o.BR[1])+v,w=Math.max(o.TL[1],o.TR[1])+v;y=Math.max(o.TL[0],o.BL[0])+u,x=Math.min(o.TR[0],o.BR[0])+u,(m=c(y,x,_,w,b,{isHorizontal:!0,constrained:!0,angle:0,anchor:"middle"})).fontSize=p.size,f(d.type,m,r),e[s].transform=m,h.attr("transform",a.getTextTransform(m))}))}}));var v=n.select(this).selectAll("g.titletext").data(d.title.text?[0]:[]);v.enter().append("g").classed("titletext",!0),v.exit().remove(),v.each((function(){var e=a.ensureSingle(n.select(this),"text","",(function(t){t.attr("data-notex",1)})),c=d.title.text;d._meta&&(c=a.templateString(c,d._meta)),e.text(c).attr({class:"titletext",transform:"","text-anchor":"middle"}).call(i.font,d.title.font).call(l.convertToTspans,t);var u=x(h,r._size);e.attr("transform",s(u.x,u.y)+o(Math.min(1,u.scale))+s(u.tx,u.ty))}))}))}))}},{"../../components/drawing":388,"../../lib":503,"../../lib/svg_text_utils":529,"../bar/plot":659,"../bar/uniform_text":664,"../pie/helpers":906,"../pie/plot":910,"@plotly/d3":58}],791:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../pie/style_one"),a=t("../bar/uniform_text").resizeText;e.exports=function(t){var e=t._fullLayout._funnelarealayer.selectAll(".trace");a(t,e,"funnelarea"),e.each((function(t){var e=t[0].trace,r=n.select(this);r.style({opacity:e.opacity}),r.selectAll("path.surface").each((function(t){n.select(this).call(i,t,e)}))}))}},{"../bar/uniform_text":664,"../pie/style_one":912,"@plotly/d3":58}],792:[function(t,e,r){"use strict";var n=t("../scatter/attributes"),i=t("../../plots/attributes"),a=t("../../plots/font_attributes"),o=t("../../plots/cartesian/axis_format_attributes").axisHoverFormat,s=t("../../plots/template_attributes").hovertemplateAttrs,l=t("../../plots/template_attributes").texttemplateAttrs,c=t("../../components/colorscale/attributes"),u=t("../../lib/extend").extendFlat;e.exports=u({z:{valType:"data_array",editType:"calc"},x:u({},n.x,{impliedEdits:{xtype:"array"}}),x0:u({},n.x0,{impliedEdits:{xtype:"scaled"}}),dx:u({},n.dx,{impliedEdits:{xtype:"scaled"}}),y:u({},n.y,{impliedEdits:{ytype:"array"}}),y0:u({},n.y0,{impliedEdits:{ytype:"scaled"}}),dy:u({},n.dy,{impliedEdits:{ytype:"scaled"}}),xperiod:u({},n.xperiod,{impliedEdits:{xtype:"scaled"}}),yperiod:u({},n.yperiod,{impliedEdits:{ytype:"scaled"}}),xperiod0:u({},n.xperiod0,{impliedEdits:{xtype:"scaled"}}),yperiod0:u({},n.yperiod0,{impliedEdits:{ytype:"scaled"}}),xperiodalignment:u({},n.xperiodalignment,{impliedEdits:{xtype:"scaled"}}),yperiodalignment:u({},n.yperiodalignment,{impliedEdits:{ytype:"scaled"}}),text:{valType:"data_array",editType:"calc"},hovertext:{valType:"data_array",editType:"calc"},transpose:{valType:"boolean",dflt:!1,editType:"calc"},xtype:{valType:"enumerated",values:["array","scaled"],editType:"calc+clearAxisTypes"},ytype:{valType:"enumerated",values:["array","scaled"],editType:"calc+clearAxisTypes"},zsmooth:{valType:"enumerated",values:["fast","best",!1],dflt:!1,editType:"calc"},hoverongaps:{valType:"boolean",dflt:!0,editType:"none"},connectgaps:{valType:"boolean",editType:"calc"},xgap:{valType:"number",dflt:0,min:0,editType:"plot"},ygap:{valType:"number",dflt:0,min:0,editType:"plot"},xhoverformat:o("x"),yhoverformat:o("y"),zhoverformat:o("z",1),hovertemplate:s(),texttemplate:l({arrayOk:!1,editType:"plot"},{keys:["x","y","z","text"]}),textfont:a({editType:"plot",autoSize:!0,autoColor:!0,colorEditType:"style"}),showlegend:u({},i.showlegend,{dflt:!1})},{transforms:void 0},c("",{cLetter:"z",autoColorDflt:!1}))},{"../../components/colorscale/attributes":373,"../../lib/extend":493,"../../plots/attributes":550,"../../plots/cartesian/axis_format_attributes":557,"../../plots/font_attributes":585,"../../plots/template_attributes":633,"../scatter/attributes":927}],793:[function(t,e,r){"use strict";var n=t("../../registry"),i=t("../../lib"),a=t("../../plots/cartesian/axes"),o=t("../../plots/cartesian/align_period"),s=t("../histogram2d/calc"),l=t("../../components/colorscale/calc"),c=t("./convert_column_xyz"),u=t("./clean_2d_array"),f=t("./interp2d"),h=t("./find_empties"),p=t("./make_bound_array"),d=t("../../constants/numerical").BADNUM;function m(t){for(var e=[],r=t.length,n=0;nD){O("x scale is not linear");break}}if(x.length&&"fast"===P){var R=(x[x.length-1]-x[0])/(x.length-1),F=Math.abs(R/100);for(k=0;kF){O("y scale is not linear");break}}}var B=i.maxRowLength(T),N="scaled"===e.xtype?"":r,j=p(e,N,g,v,B,M),U="scaled"===e.ytype?"":x,V=p(e,U,b,_,T.length,S);C||(e._extremes[M._id]=a.findExtremes(M,j),e._extremes[S._id]=a.findExtremes(S,V));var H={x:j,y:V,z:T,text:e._text||e.text,hovertext:e._hovertext||e.hovertext};if(e.xperiodalignment&&y&&(H.orig_x=y),e.yperiodalignment&&w&&(H.orig_y=w),N&&N.length===j.length-1&&(H.xCenter=N),U&&U.length===V.length-1&&(H.yCenter=U),L&&(H.xRanges=A.xRanges,H.yRanges=A.yRanges,H.pts=A.pts),E||l(t,e,{vals:T,cLetter:"z"}),E&&e.contours&&"heatmap"===e.contours.coloring){var q={type:"contour"===e.type?"heatmap":"histogram2d",xcalendar:e.xcalendar,ycalendar:e.ycalendar};H.xfill=p(q,N,g,v,B,M),H.yfill=p(q,U,b,_,T.length,S)}return[H]}},{"../../components/colorscale/calc":374,"../../constants/numerical":479,"../../lib":503,"../../plots/cartesian/align_period":551,"../../plots/cartesian/axes":554,"../../registry":638,"../histogram2d/calc":826,"./clean_2d_array":794,"./convert_column_xyz":796,"./find_empties":798,"./interp2d":801,"./make_bound_array":803}],794:[function(t,e,r){"use strict";var n=t("fast-isnumeric"),i=t("../../lib"),a=t("../../constants/numerical").BADNUM;e.exports=function(t,e,r,o){var s,l,c,u,f,h;function p(t){if(n(t))return+t}if(e&&e.transpose){for(s=0,f=0;f=0;o--)(s=((f[[(r=(a=h[o])[0])-1,i=a[1]]]||m)[2]+(f[[r+1,i]]||m)[2]+(f[[r,i-1]]||m)[2]+(f[[r,i+1]]||m)[2])/20)&&(l[a]=[r,i,s],h.splice(o,1),c=!0);if(!c)throw"findEmpties iterated with no new neighbors";for(a in l)f[a]=l[a],u.push(l[a])}return u.sort((function(t,e){return e[2]-t[2]}))}},{"../../lib":503}],799:[function(t,e,r){"use strict";var n=t("../../components/fx"),i=t("../../lib"),a=t("../../plots/cartesian/axes"),o=t("../../components/colorscale").extractOpts;e.exports=function(t,e,r,s,l){l||(l={});var c,u,f,h,p=l.isContour,d=t.cd[0],m=d.trace,g=t.xa,v=t.ya,y=d.x,x=d.y,b=d.z,_=d.xCenter,w=d.yCenter,T=d.zmask,k=m.zhoverformat,A=y,M=x;if(!1!==t.index){try{f=Math.round(t.index[1]),h=Math.round(t.index[0])}catch(e){return void i.error("Error hovering on heatmap, pointNumber must be [row,col], found:",t.index)}if(f<0||f>=b[0].length||h<0||h>b.length)return}else{if(n.inbox(e-y[0],e-y[y.length-1],0)>0||n.inbox(r-x[0],r-x[x.length-1],0)>0)return;if(p){var S;for(A=[2*y[0]-y[1]],S=1;Sm&&(v=Math.max(v,Math.abs(t[a][o]-d)/(g-m))))}return v}e.exports=function(t,e){var r,i=1;for(o(t,e),r=0;r.01;r++)i=o(t,e,a(i));return i>.01&&n.log("interp2d didn't converge quickly",i),t}},{"../../lib":503}],802:[function(t,e,r){"use strict";var n=t("../../lib");e.exports=function(t,e){t("texttemplate");var r=n.extendFlat({},e.font,{color:"auto",size:"auto"});n.coerceFont(t,"textfont",r)}},{"../../lib":503}],803:[function(t,e,r){"use strict";var n=t("../../registry"),i=t("../../lib").isArrayOrTypedArray;e.exports=function(t,e,r,a,o,s){var l,c,u,f=[],h=n.traceIs(t,"contour"),p=n.traceIs(t,"histogram"),d=n.traceIs(t,"gl2d");if(i(e)&&e.length>1&&!p&&"category"!==s.type){var m=e.length;if(!(m<=o))return h?e.slice(0,o):e.slice(0,o+1);if(h||d)f=e.slice(0,o);else if(1===o)f=[e[0]-.5,e[0]+.5];else{for(f=[1.5*e[0]-.5*e[1]],u=1;u0;)_=w.c2p(R[S]),S--;for(_0;)M=T.c2p(F[S]),S--;if(MGt||Gt>T._length))for(E=Ut;EWt||Wt>w._length)){var Xt=u({x:Yt,y:qt},I,t._fullLayout);Xt.x=Yt,Xt.y=qt;var Zt=P.z[S][E];void 0===Zt?(Xt.z="",Xt.zLabel=""):(Xt.z=Zt,Xt.zLabel=s.tickText(Ft,Zt,"hover").text);var Jt=P.text&&P.text[S]&&P.text[S][E];void 0!==Jt&&!1!==Jt||(Jt=""),Xt.text=Jt;var Kt=l.texttemplateString(Dt,Xt,t._fullLayout._d3locale,Xt,I._meta||{});if(Kt){var Qt=Kt.split("
"),$t=Qt.length,te=0;for(L=0;L<$t;L++)te=Math.max(te,Qt[L].length);Ht.push({l:$t,c:te,t:Kt,x:Wt,y:Gt,z:Zt})}}}}var ee=I.textfont,re=ee.family,ne=ee.size,ie=t._fullLayout.font.size;if(!ne||"auto"===ne){var ae=1/0,oe=1/0,se=0,le=0;for(L=0;L0&&(a=!0);for(var l=0;la){var o=a-r[t];return r[t]=a,o}}return 0},max:function(t,e,r,i){var a=i[e];if(n(a)){if(a=Number(a),!n(r[t]))return r[t]=a,a;if(r[t]c?t>o?t>1.1*i?i:t>1.1*a?a:o:t>s?s:t>l?l:c:Math.pow(10,Math.floor(Math.log(t)/Math.LN10))}function p(t,e,r,n,a,s){if(n&&t>o){var l=d(e,a,s),c=d(r,a,s),u=t===i?0:1;return l[u]!==c[u]}return Math.floor(r/t)-Math.floor(e/t)>.1}function d(t,e,r){var n=e.c2d(t,i,r).split("-");return""===n[0]&&(n.unshift(),n[0]="-"+n[0]),n}e.exports=function(t,e,r,n,a){var s,l,c=-1.1*e,h=-.1*e,p=t-h,d=r[0],m=r[1],g=Math.min(f(d+h,d+p,n,a),f(m+h,m+p,n,a)),v=Math.min(f(d+c,d+h,n,a),f(m+c,m+h,n,a));if(g>v&&vo){var y=s===i?1:6,x=s===i?"M12":"M1";return function(e,r){var o=n.c2d(e,i,a),s=o.indexOf("-",y);s>0&&(o=o.substr(0,s));var c=n.d2c(o,0,a);if(cr.r2l(B)&&(j=o.tickIncrement(j,b.size,!0,p)),z.start=r.l2r(j),F||i.nestedProperty(e,v+".start").set(z.start)}var U=b.end,V=r.r2l(O.end),H=void 0!==V;if((b.endFound||H)&&V!==r.r2l(U)){var q=H?V:i.aggNums(Math.max,null,d);z.end=r.l2r(q),H||i.nestedProperty(e,v+".start").set(z.end)}var G="autobin"+s;return!1===e._input[G]&&(e._input[v]=i.extendFlat({},e[v]||{}),delete e._input[G],delete e[G]),[z,d]}e.exports={calc:function(t,e){var r,a,p,d,m=[],g=[],v="h"===e.orientation,y=o.getFromId(t,v?e.yaxis:e.xaxis),x=v?"y":"x",b={x:"y",y:"x"}[x],_=e[x+"calendar"],w=e.cumulative,T=h(t,e,y,x),k=T[0],A=T[1],M="string"==typeof k.size,S=[],E=M?S:k,L=[],C=[],P=[],I=0,O=e.histnorm,z=e.histfunc,D=-1!==O.indexOf("density");w.enabled&&D&&(O=O.replace(/ ?density$/,""),D=!1);var R,F="max"===z||"min"===z?null:0,B=l.count,N=c[O],j=!1,U=function(t){return y.r2c(t,0,_)};for(i.isArrayOrTypedArray(e[b])&&"count"!==z&&(R=e[b],j="avg"===z,B=l[z]),r=U(k.start),p=U(k.end)+(r-o.tickIncrement(r,k.size,!1,_))/1e6;r=0&&d=0;n--)s(n);else if("increasing"===e){for(n=1;n=0;n--)t[n]+=t[n+1];"exclude"===r&&(t.push(0),t.shift())}}(g,w.direction,w.currentbin);var K=Math.min(m.length,g.length),Q=[],$=0,tt=K-1;for(r=0;r=$;r--)if(g[r]){tt=r;break}for(r=$;r<=tt;r++)if(n(m[r])&&n(g[r])){var et={p:m[r],s:g[r],b:0};w.enabled||(et.pts=P[r],Y?et.ph0=et.ph1=P[r].length?A[P[r][0]]:m[r]:(e._computePh=!0,et.ph0=q(S[r]),et.ph1=q(S[r+1],!0))),Q.push(et)}return 1===Q.length&&(Q[0].width1=o.tickIncrement(Q[0].p,k.size,!1,_)-Q[0].p),s(Q,e),i.isArrayOrTypedArray(e.selectedpoints)&&i.tagSelected(Q,e,Z),Q},calcAllAutoBins:h}},{"../../lib":503,"../../plots/cartesian/axes":554,"../../registry":638,"../bar/arrays_to_calcdata":647,"./average":813,"./bin_functions":815,"./bin_label_vals":816,"./norm_functions":824,"fast-isnumeric":190}],818:[function(t,e,r){"use strict";e.exports={eventDataKeys:["binNumber"]}},{}],819:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("../../plots/cartesian/axis_ids"),a=t("../../registry").traceIs,o=t("../bar/defaults").handleGroupingDefaults,s=n.nestedProperty,l=t("../../plots/cartesian/constraints").getAxisGroup,c=[{aStr:{x:"xbins.start",y:"ybins.start"},name:"start"},{aStr:{x:"xbins.end",y:"ybins.end"},name:"end"},{aStr:{x:"xbins.size",y:"ybins.size"},name:"size"},{aStr:{x:"nbinsx",y:"nbinsy"},name:"nbins"}],u=["x","y"];e.exports=function(t,e){var r,f,h,p,d,m,g,v=e._histogramBinOpts={},y=[],x={},b=[];function _(t,e){return n.coerce(r._input,r,r._module.attributes,t,e)}function w(t){return"v"===t.orientation?"x":"y"}function T(t,r,a){var o=t.uid+"__"+a;r||(r=o);var s=function(t,r){return i.getFromTrace({_fullLayout:e},t,r).type}(t,a),l=t[a+"calendar"]||"",c=v[r],u=!0;c&&(s===c.axType&&l===c.calendar?(u=!1,c.traces.push(t),c.dirs.push(a)):(r=o,s!==c.axType&&n.warn(["Attempted to group the bins of trace",t.index,"set on a","type:"+s,"axis","with bins on","type:"+c.axType,"axis."].join(" ")),l!==c.calendar&&n.warn(["Attempted to group the bins of trace",t.index,"set with a",l,"calendar","with bins",c.calendar?"on a "+c.calendar+" calendar":"w/o a set calendar"].join(" ")))),u&&(v[r]={traces:[t],dirs:[a],axType:s,calendar:t[a+"calendar"]||""}),t["_"+a+"bingroup"]=r}for(d=0;dS&&T.splice(S,T.length-S),M.length>S&&M.splice(S,M.length-S);var E=[],L=[],C=[],P="string"==typeof w.size,I="string"==typeof A.size,O=[],z=[],D=P?O:w,R=I?z:A,F=0,B=[],N=[],j=e.histnorm,U=e.histfunc,V=-1!==j.indexOf("density"),H="max"===U||"min"===U?null:0,q=a.count,G=o[j],Y=!1,W=[],X=[],Z="z"in e?e.z:"marker"in e&&Array.isArray(e.marker.color)?e.marker.color:"";Z&&"count"!==U&&(Y="avg"===U,q=a[U]);var J=w.size,K=x(w.start),Q=x(w.end)+(K-i.tickIncrement(K,J,!1,v))/1e6;for(r=K;r=0&&p=0&&d-1,flipY:E.tiling.flip.indexOf("y")>-1,orientation:E.tiling.orientation,pad:{inner:E.tiling.pad},maxDepth:E._maxDepth}).descendants(),O=1/0,z=-1/0;I.forEach((function(t){var e=t.depth;e>=E._maxDepth?(t.x0=t.x1=(t.x0+t.x1)/2,t.y0=t.y1=(t.y0+t.y1)/2):(O=Math.min(O,e),z=Math.max(z,e))})),p=p.data(I,u.getPtId),E._maxVisibleLayers=isFinite(z)?z-O+1:0,p.enter().append("g").classed("slice",!0),T(p,!1,{},[m,g],x),p.order();var D=null;if(w&&M){var R=u.getPtId(M);p.each((function(t){null===D&&u.getPtId(t)===R&&(D={x0:t.x0,x1:t.x1,y0:t.y0,y1:t.y1})}))}var F=function(){return D||{x0:0,x1:m,y0:0,y1:g}},B=p;return w&&(B=B.transition().each("end",(function(){var e=n.select(this);u.setSliceCursor(e,t,{hideOnRoot:!0,hideOnLeaves:!1,isTransitioning:!1})}))),B.each((function(s){s._x0=v(s.x0),s._x1=v(s.x1),s._y0=y(s.y0),s._y1=y(s.y1),s._hoverX=v(s.x1-E.tiling.pad),s._hoverY=y(P?s.y1-E.tiling.pad/2:s.y0+E.tiling.pad/2);var p=n.select(this),d=i.ensureSingle(p,"path","surface",(function(t){t.style("pointer-events","all")}));w?d.transition().attrTween("d",(function(t){var e=k(t,!1,F(),[m,g],{orientation:E.tiling.orientation,flipX:E.tiling.flip.indexOf("x")>-1,flipY:E.tiling.flip.indexOf("y")>-1});return function(t){return x(e(t))}})):d.attr("d",x),p.call(f,r,t,e,{styleOne:l,eventDataKeys:c.eventDataKeys,transitionTime:c.CLICK_TRANSITION_TIME,transitionEasing:c.CLICK_TRANSITION_EASING}).call(u.setSliceCursor,t,{isTransitioning:t._transitioning}),d.call(l,s,E,{hovered:!1}),s.x0===s.x1||s.y0===s.y1?s._text="":s._text=h(s,r,E,e,S)||"";var T=i.ensureSingle(p,"g","slicetext"),M=i.ensureSingle(T,"text","",(function(t){t.attr("data-notex",1)})),I=i.ensureUniformFontSize(t,u.determineTextFont(E,s,S.font));M.text(s._text||" ").classed("slicetext",!0).attr("text-anchor",C?"end":L?"start":"middle").call(a.font,I).call(o.convertToTspans,t),s.textBB=a.bBox(M.node()),s.transform=b(s,{fontSize:I.size}),s.transform.fontSize=I.size,w?M.transition().attrTween("transform",(function(t){var e=A(t,!1,F(),[m,g]);return function(t){return _(e(t))}})):M.attr("transform",_(s))})),D}},{"../../components/drawing":388,"../../lib":503,"../../lib/svg_text_utils":529,"../sunburst/fx":1054,"../sunburst/helpers":1055,"../sunburst/plot":1059,"../treemap/constants":1078,"./partition":842,"./style":844,"@plotly/d3":58}],839:[function(t,e,r){"use strict";e.exports={moduleType:"trace",name:"icicle",basePlotModule:t("./base_plot"),categories:[],animatable:!0,attributes:t("./attributes"),layoutAttributes:t("./layout_attributes"),supplyDefaults:t("./defaults"),supplyLayoutDefaults:t("./layout_defaults"),calc:t("./calc").calc,crossTraceCalc:t("./calc").crossTraceCalc,plot:t("./plot"),style:t("./style").style,colorbar:t("../scatter/marker_colorbar"),meta:{}}},{"../scatter/marker_colorbar":945,"./attributes":834,"./base_plot":835,"./calc":836,"./defaults":837,"./layout_attributes":840,"./layout_defaults":841,"./plot":843,"./style":844}],840:[function(t,e,r){"use strict";e.exports={iciclecolorway:{valType:"colorlist",editType:"calc"},extendiciclecolors:{valType:"boolean",dflt:!0,editType:"calc"}}},{}],841:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("./layout_attributes");e.exports=function(t,e){function r(r,a){return n.coerce(t,e,i,r,a)}r("iciclecolorway",e.colorway),r("extendiciclecolors")}},{"../../lib":503,"./layout_attributes":840}],842:[function(t,e,r){"use strict";var n=t("d3-hierarchy"),i=t("../treemap/flip_tree");e.exports=function(t,e,r){var a=r.flipX,o=r.flipY,s="h"===r.orientation,l=r.maxDepth,c=e[0],u=e[1];l&&(c=(t.height+1)*e[0]/Math.min(t.height+1,l),u=(t.height+1)*e[1]/Math.min(t.height+1,l));var f=n.partition().padding(r.pad.inner).size(s?[e[1],c]:[e[0],u])(t);return(s||a||o)&&i(f,e,{swapXY:s,flipX:a,flipY:o}),f}},{"../treemap/flip_tree":1083,"d3-hierarchy":115}],843:[function(t,e,r){"use strict";var n=t("../treemap/draw"),i=t("./draw_descendants");e.exports=function(t,e,r,a){return n(t,e,r,a,{type:"icicle",drawDescendants:i})}},{"../treemap/draw":1080,"./draw_descendants":838}],844:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../components/color"),a=t("../../lib"),o=t("../bar/uniform_text").resizeText;function s(t,e,r){var n=e.data.data,o=!e.children,s=n.i,l=a.castOption(r,s,"marker.line.color")||i.defaultLine,c=a.castOption(r,s,"marker.line.width")||0;t.style("stroke-width",c).call(i.fill,n.color).call(i.stroke,l).style("opacity",o?r.leaf.opacity:null)}e.exports={style:function(t){var e=t._fullLayout._iciclelayer.selectAll(".trace");o(t,e,"icicle"),e.each((function(t){var e=n.select(this),r=t[0].trace;e.style("opacity",r.opacity),e.selectAll("path.surface").each((function(t){n.select(this).call(s,t,r)}))}))},styleOne:s}},{"../../components/color":366,"../../lib":503,"../bar/uniform_text":664,"@plotly/d3":58}],845:[function(t,e,r){"use strict";for(var n=t("../../plots/attributes"),i=t("../../plots/template_attributes").hovertemplateAttrs,a=t("../../lib/extend").extendFlat,o=t("./constants").colormodel,s=["rgb","rgba","rgba256","hsl","hsla"],l=[],c=[],u=0;u0||n.inbox(r-o.y0,r-(o.y0+o.h*s.dy),0)>0)){var u,f=Math.floor((e-o.x0)/s.dx),h=Math.floor(Math.abs(r-o.y0)/s.dy);if(s._hasZ?u=o.z[h][f]:s._hasSource&&(u=s._canvas.el.getContext("2d",{willReadFrequently:!0}).getImageData(f,h,1,1).data),u){var p,d=o.hi||s.hoverinfo;if(d){var m=d.split("+");-1!==m.indexOf("all")&&(m=["color"]),-1!==m.indexOf("color")&&(p=!0)}var g,v=a.colormodel[s.colormodel],y=v.colormodel||s.colormodel,x=y.length,b=s._scaler(u),_=v.suffix,w=[];(s.hovertemplate||p)&&(w.push("["+[b[0]+_[0],b[1]+_[1],b[2]+_[2]].join(", ")),4===x&&w.push(", "+b[3]+_[3]),w.push("]"),w=w.join(""),t.extraText=y.toUpperCase()+": "+w),Array.isArray(s.hovertext)&&Array.isArray(s.hovertext[h])?g=s.hovertext[h][f]:Array.isArray(s.text)&&Array.isArray(s.text[h])&&(g=s.text[h][f]);var T=c.c2p(o.y0+(h+.5)*s.dy),k=o.x0+(f+.5)*s.dx,A=o.y0+(h+.5)*s.dy,M="["+u.slice(0,s.colormodel.length).join(", ")+"]";return[i.extendFlat(t,{index:[h,f],x0:l.c2p(o.x0+f*s.dx),x1:l.c2p(o.x0+(f+1)*s.dx),y0:T,y1:T,color:b,xVal:k,xLabelVal:k,yVal:A,yLabelVal:A,zLabelVal:M,text:g,hovertemplateLabels:{zLabel:M,colorLabel:w,"color[0]Label":b[0]+_[0],"color[1]Label":b[1]+_[1],"color[2]Label":b[2]+_[2],"color[3]Label":b[3]+_[3]}})]}}}},{"../../components/fx":406,"../../lib":503,"./constants":847}],852:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),supplyDefaults:t("./defaults"),calc:t("./calc"),plot:t("./plot"),style:t("./style"),hoverPoints:t("./hover"),eventData:t("./event_data"),moduleType:"trace",name:"image",basePlotModule:t("../../plots/cartesian"),categories:["cartesian","svg","2dMap","noSortingByValue"],animatable:!1,meta:{}}},{"../../plots/cartesian":568,"./attributes":845,"./calc":846,"./defaults":848,"./event_data":849,"./hover":851,"./plot":853,"./style":854}],853:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../lib"),a=i.strTranslate,o=t("../../constants/xmlns_namespaces"),s=t("./constants"),l=i.isIOS()||i.isSafari()||i.isIE();e.exports=function(t,e,r,c){var u=e.xaxis,f=e.yaxis,h=!(l||t._context._exportedPlot);i.makeTraceGroups(c,r,"im").each((function(e){var r=n.select(this),l=e[0],c=l.trace,p=("fast"===c.zsmooth||!1===c.zsmooth&&h)&&!c._hasZ&&c._hasSource&&"linear"===u.type&&"linear"===f.type;c._realImage=p;var d,m,g,v,y,x,b=l.z,_=l.x0,w=l.y0,T=l.w,k=l.h,A=c.dx,M=c.dy;for(x=0;void 0===d&&x0;)m=u.c2p(_+x*A),x--;for(x=0;void 0===v&&x0;)y=f.c2p(w+x*M),x--;if(mI[0];if(O||z){var D=d+S/2,R=v+E/2;C+="transform:"+a(D+"px",R+"px")+"scale("+(O?-1:1)+","+(z?-1:1)+")"+a(-D+"px",-R+"px")+";"}}L.attr("style",C);var F=new Promise((function(t){if(c._hasZ)t();else if(c._hasSource)if(c._canvas&&c._canvas.el.width===T&&c._canvas.el.height===k&&c._canvas.source===c.source)t();else{var e=document.createElement("canvas");e.width=T,e.height=k;var r=e.getContext("2d",{willReadFrequently:!0});c._image=c._image||new Image;var n=c._image;n.onload=function(){r.drawImage(n,0,0),c._canvas={el:e,source:c.source},t()},n.setAttribute("src",c.source)}})).then((function(){var t;if(c._hasZ)t=B((function(t,e){return b[e][t]})).toDataURL("image/png");else if(c._hasSource)if(p)t=c.source;else{var e=c._canvas.el.getContext("2d",{willReadFrequently:!0}).getImageData(0,0,T,k).data;t=B((function(t,r){var n=4*(r*T+t);return[e[n],e[n+1],e[n+2],e[n+3]]})).toDataURL("image/png")}L.attr({"xlink:href":t,height:E,width:S,x:d,y:v})}));t._promises.push(F)}function B(t){var e=document.createElement("canvas");e.width=S,e.height=E;var r,n=e.getContext("2d",{willReadFrequently:!0}),a=function(t){return i.constrain(Math.round(u.c2p(_+t*A)-d),0,S)},o=function(t){return i.constrain(Math.round(f.c2p(w+t*M)-v),0,E)},h=s.colormodel[c.colormodel],p=h.colormodel||c.colormodel,m=h.fmt;for(x=0;x0}function T(t){t.each((function(t){y.stroke(n.select(this),t.line.color)})).each((function(t){y.fill(n.select(this),t.color)})).style("stroke-width",(function(t){return t.line.width}))}function k(t,e,r){var n=t._fullLayout,i=o.extendFlat({type:"linear",ticks:"outside",range:r,showline:!0},e),a={type:"linear",_id:"x"+e._id},s={letter:"x",font:n.font,noHover:!0,noTickson:!0};function l(t,e){return o.coerce(i,a,v,t,e)}return m(i,a,l,s,n),g(i,a,l,s),a}function A(t,e,r){return[Math.min(e/t.width,r/t.height),t,e+"x"+r]}function M(t,e,r,i){var a=document.createElementNS("http://www.w3.org/2000/svg","text"),o=n.select(a);return o.text(t).attr("x",0).attr("y",0).attr("text-anchor",r).attr("data-unformatted",t).call(p.convertToTspans,i).call(f.font,e),f.bBox(o.node())}function S(t,e,r,n,i,a){var s="_cache"+e;t[s]&&t[s].key===i||(t[s]={key:i,value:r});var l=o.aggNums(a,null,[t[s].value,n],2);return t[s].value=l,l}e.exports=function(t,e,r,m){var g,v=t._fullLayout;w(r)&&m&&(g=m()),o.makeTraceGroups(v._indicatorlayer,e,"trace").each((function(e){var m,E,L,C,P,I=e[0].trace,O=n.select(this),z=I._hasGauge,D=I._isAngular,R=I._isBullet,F=I.domain,B={w:v._size.w*(F.x[1]-F.x[0]),h:v._size.h*(F.y[1]-F.y[0]),l:v._size.l+v._size.w*F.x[0],r:v._size.r+v._size.w*(1-F.x[1]),t:v._size.t+v._size.h*(1-F.y[1]),b:v._size.b+v._size.h*F.y[0]},N=B.l+B.w/2,j=B.t+B.h/2,U=Math.min(B.w/2,B.h),V=h.innerRadius*U,H=I.align||"center";if(E=j,z){if(D&&(m=N,E=j+U/2,L=function(t){return function(t,e){var r=Math.sqrt(t.width/2*(t.width/2)+t.height*t.height);return[e/r,t,e]}(t,.9*V)}),R){var q=h.bulletPadding,G=1-h.bulletNumberDomainSize+q;m=B.l+(G+(1-G)*b[H])*B.w,L=function(t){return A(t,(h.bulletNumberDomainSize-q)*B.w,B.h)}}}else m=B.l+b[H]*B.w,L=function(t){return A(t,B.w,B.h)};!function(t,e,r,i){var c,u,h,m=r[0].trace,g=i.numbersX,v=i.numbersY,T=m.align||"center",A=x[T],E=i.transitionOpts,L=i.onComplete,C=o.ensureSingle(e,"g","numbers"),P=[];m._hasNumber&&P.push("number");m._hasDelta&&(P.push("delta"),"left"===m.delta.position&&P.reverse());var I=C.selectAll("text").data(P);function O(e,r,n,i){if(!e.match("s")||n>=0==i>=0||r(n).slice(-1).match(_)||r(i).slice(-1).match(_))return r;var a=e.slice().replace("s","f").replace(/\d+/,(function(t){return parseInt(t)-1})),o=k(t,{tickformat:a});return function(t){return Math.abs(t)<1?d.tickText(o,t).text:r(t)}}I.enter().append("text"),I.attr("text-anchor",(function(){return A})).attr("class",(function(t){return t})).attr("x",null).attr("y",null).attr("dx",null).attr("dy",null),I.exit().remove();var z,D=m.mode+m.align;m._hasDelta&&(z=function(){var e=k(t,{tickformat:m.delta.valueformat},m._range);e.setScale(),d.prepTicks(e);var i=function(t){return d.tickText(e,t).text},o=function(t){return m.delta.relative?t.relativeDelta:t.delta},s=function(t,e){return 0===t||"number"!=typeof t||isNaN(t)?"-":(t>0?m.delta.increasing.symbol:m.delta.decreasing.symbol)+e(t)},l=function(t){return t.delta>=0?m.delta.increasing.color:m.delta.decreasing.color};void 0===m._deltaLastValue&&(m._deltaLastValue=o(r[0]));var c=C.select("text.delta");function h(){c.text(s(o(r[0]),i)).call(y.fill,l(r[0])).call(p.convertToTspans,t)}return c.call(f.font,m.delta.font).call(y.fill,l({delta:m._deltaLastValue})),w(E)?c.transition().duration(E.duration).ease(E.easing).tween("text",(function(){var t=n.select(this),e=o(r[0]),c=m._deltaLastValue,u=O(m.delta.valueformat,i,c,e),f=a(c,e);return m._deltaLastValue=e,function(e){t.text(s(f(e),u)),t.call(y.fill,l({delta:f(e)}))}})).each("end",(function(){h(),L&&L()})).each("interrupt",(function(){h(),L&&L()})):h(),u=M(s(o(r[0]),i),m.delta.font,A,t),c}(),D+=m.delta.position+m.delta.font.size+m.delta.font.family+m.delta.valueformat,D+=m.delta.increasing.symbol+m.delta.decreasing.symbol,h=u);m._hasNumber&&(!function(){var e=k(t,{tickformat:m.number.valueformat},m._range);e.setScale(),d.prepTicks(e);var i=function(t){return d.tickText(e,t).text},o=m.number.suffix,s=m.number.prefix,l=C.select("text.number");function u(){var e="number"==typeof r[0].y?s+i(r[0].y)+o:"-";l.text(e).call(f.font,m.number.font).call(p.convertToTspans,t)}w(E)?l.transition().duration(E.duration).ease(E.easing).each("end",(function(){u(),L&&L()})).each("interrupt",(function(){u(),L&&L()})).attrTween("text",(function(){var t=n.select(this),e=a(r[0].lastY,r[0].y);m._lastValue=r[0].y;var l=O(m.number.valueformat,i,r[0].lastY,r[0].y);return function(r){t.text(s+l(e(r))+o)}})):u(),c=M(s+i(r[0].y)+o,m.number.font,A,t)}(),D+=m.number.font.size+m.number.font.family+m.number.valueformat+m.number.suffix+m.number.prefix,h=c);if(m._hasDelta&&m._hasNumber){var R,F,B=[(c.left+c.right)/2,(c.top+c.bottom)/2],N=[(u.left+u.right)/2,(u.top+u.bottom)/2],j=.75*m.delta.font.size;"left"===m.delta.position&&(R=S(m,"deltaPos",0,-1*(c.width*b[m.align]+u.width*(1-b[m.align])+j),D,Math.min),F=B[1]-N[1],h={width:c.width+u.width+j,height:Math.max(c.height,u.height),left:u.left+R,right:c.right,top:Math.min(c.top,u.top+F),bottom:Math.max(c.bottom,u.bottom+F)}),"right"===m.delta.position&&(R=S(m,"deltaPos",0,c.width*(1-b[m.align])+u.width*b[m.align]+j,D,Math.max),F=B[1]-N[1],h={width:c.width+u.width+j,height:Math.max(c.height,u.height),left:c.left,right:u.right+R,top:Math.min(c.top,u.top+F),bottom:Math.max(c.bottom,u.bottom+F)}),"bottom"===m.delta.position&&(R=null,F=u.height,h={width:Math.max(c.width,u.width),height:c.height+u.height,left:Math.min(c.left,u.left),right:Math.max(c.right,u.right),top:c.bottom-c.height,bottom:c.bottom+u.height}),"top"===m.delta.position&&(R=null,F=c.top,h={width:Math.max(c.width,u.width),height:c.height+u.height,left:Math.min(c.left,u.left),right:Math.max(c.right,u.right),top:c.bottom-c.height-u.height,bottom:c.bottom}),z.attr({dx:R,dy:F})}(m._hasNumber||m._hasDelta)&&C.attr("transform",(function(){var t=i.numbersScaler(h);D+=t[2];var e,r=S(m,"numbersScale",1,t[0],D,Math.min);m._scaleNumbers||(r=1),e=m._isAngular?v-r*h.bottom:v-r*(h.top+h.bottom)/2,m._numbersTop=r*h.top+e;var n=h[T];"center"===T&&(n=(h.left+h.right)/2);var a=g-r*n;return a=S(m,"numbersTranslate",0,a,D,Math.max),l(a,e)+s(r)}))}(t,O,e,{numbersX:m,numbersY:E,numbersScaler:L,transitionOpts:r,onComplete:g}),z&&(C={range:I.gauge.axis.range,color:I.gauge.bgcolor,line:{color:I.gauge.bordercolor,width:0},thickness:1},P={range:I.gauge.axis.range,color:"rgba(0, 0, 0, 0)",line:{color:I.gauge.bordercolor,width:I.gauge.borderwidth},thickness:1});var Y=O.selectAll("g.angular").data(D?e:[]);Y.exit().remove();var W=O.selectAll("g.angularaxis").data(D?e:[]);W.exit().remove(),D&&function(t,e,r,a){var o,s,f,h,p=r[0].trace,m=a.size,g=a.radius,v=a.innerRadius,y=a.gaugeBg,x=a.gaugeOutline,b=[m.l+m.w/2,m.t+m.h/2+g/2],_=a.gauge,A=a.layer,M=a.transitionOpts,S=a.onComplete,E=Math.PI/2;function L(t){var e=p.gauge.axis.range[0],r=(t-e)/(p.gauge.axis.range[1]-e)*Math.PI-E;return r<-E?-E:r>E?E:r}function C(t){return n.svg.arc().innerRadius((v+g)/2-t/2*(g-v)).outerRadius((v+g)/2+t/2*(g-v)).startAngle(-E)}function P(t){t.attr("d",(function(t){return C(t.thickness).startAngle(L(t.range[0])).endAngle(L(t.range[1]))()}))}_.enter().append("g").classed("angular",!0),_.attr("transform",l(b[0],b[1])),A.enter().append("g").classed("angularaxis",!0).classed("crisp",!0),A.selectAll("g.xangularaxistick,path,text").remove(),(o=k(t,p.gauge.axis)).type="linear",o.range=p.gauge.axis.range,o._id="xangularaxis",o.ticklabeloverflow="allow",o.setScale();var I=function(t){return(o.range[0]-t.x)/(o.range[1]-o.range[0])*Math.PI+Math.PI},O={},z=d.makeLabelFns(o,0).labelStandoff;O.xFn=function(t){var e=I(t);return Math.cos(e)*z},O.yFn=function(t){var e=I(t),r=Math.sin(e)>0?.2:1;return-Math.sin(e)*(z+t.fontSize*r)+Math.abs(Math.cos(e))*(t.fontSize*u)},O.anchorFn=function(t){var e=I(t),r=Math.cos(e);return Math.abs(r)<.1?"middle":r>0?"start":"end"},O.heightFn=function(t,e,r){var n=I(t);return-.5*(1+Math.sin(n))*r};var D=function(t){return l(b[0]+g*Math.cos(t),b[1]-g*Math.sin(t))};f=function(t){return D(I(t))};if(s=d.calcTicks(o),h=d.getTickSigns(o)[2],o.visible){h="inside"===o.ticks?-1:1;var R=(o.linewidth||1)/2;d.drawTicks(t,o,{vals:s,layer:A,path:"M"+h*R+",0h"+h*o.ticklen,transFn:function(t){var e=I(t);return D(e)+"rotate("+-c(e)+")"}}),d.drawLabels(t,o,{vals:s,layer:A,transFn:f,labelFns:O})}var F=[y].concat(p.gauge.steps),B=_.selectAll("g.bg-arc").data(F);B.enter().append("g").classed("bg-arc",!0).append("path"),B.select("path").call(P).call(T),B.exit().remove();var N=C(p.gauge.bar.thickness),j=_.selectAll("g.value-arc").data([p.gauge.bar]);j.enter().append("g").classed("value-arc",!0).append("path");var U=j.select("path");w(M)?(U.transition().duration(M.duration).ease(M.easing).each("end",(function(){S&&S()})).each("interrupt",(function(){S&&S()})).attrTween("d",(V=N,H=L(r[0].lastY),q=L(r[0].y),function(){var t=i(H,q);return function(e){return V.endAngle(t(e))()}})),p._lastValue=r[0].y):U.attr("d","number"==typeof r[0].y?N.endAngle(L(r[0].y)):"M0,0Z");var V,H,q;U.call(T),j.exit().remove(),F=[];var G=p.gauge.threshold.value;(G||0===G)&&F.push({range:[G,G],color:p.gauge.threshold.color,line:{color:p.gauge.threshold.line.color,width:p.gauge.threshold.line.width},thickness:p.gauge.threshold.thickness});var Y=_.selectAll("g.threshold-arc").data(F);Y.enter().append("g").classed("threshold-arc",!0).append("path"),Y.select("path").call(P).call(T),Y.exit().remove();var W=_.selectAll("g.gauge-outline").data([x]);W.enter().append("g").classed("gauge-outline",!0).append("path"),W.select("path").call(P).call(T),W.exit().remove()}(t,0,e,{radius:U,innerRadius:V,gauge:Y,layer:W,size:B,gaugeBg:C,gaugeOutline:P,transitionOpts:r,onComplete:g});var X=O.selectAll("g.bullet").data(R?e:[]);X.exit().remove();var Z=O.selectAll("g.bulletaxis").data(R?e:[]);Z.exit().remove(),R&&function(t,e,r,n){var i,a,o,s,c,u=r[0].trace,f=n.gauge,p=n.layer,m=n.gaugeBg,g=n.gaugeOutline,v=n.size,x=u.domain,b=n.transitionOpts,_=n.onComplete;f.enter().append("g").classed("bullet",!0),f.attr("transform",l(v.l,v.t)),p.enter().append("g").classed("bulletaxis",!0).classed("crisp",!0),p.selectAll("g.xbulletaxistick,path,text").remove();var A=v.h,M=u.gauge.bar.thickness*A,S=x.x[0],E=x.x[0]+(x.x[1]-x.x[0])*(u._hasNumber||u._hasDelta?1-h.bulletNumberDomainSize:1);(i=k(t,u.gauge.axis))._id="xbulletaxis",i.domain=[S,E],i.setScale(),a=d.calcTicks(i),o=d.makeTransTickFn(i),s=d.getTickSigns(i)[2],c=v.t+v.h,i.visible&&(d.drawTicks(t,i,{vals:"inside"===i.ticks?d.clipEnds(i,a):a,layer:p,path:d.makeTickPath(i,c,s),transFn:o}),d.drawLabels(t,i,{vals:a,layer:p,transFn:o,labelFns:d.makeLabelFns(i,c)}));function L(t){t.attr("width",(function(t){return Math.max(0,i.c2p(t.range[1])-i.c2p(t.range[0]))})).attr("x",(function(t){return i.c2p(t.range[0])})).attr("y",(function(t){return.5*(1-t.thickness)*A})).attr("height",(function(t){return t.thickness*A}))}var C=[m].concat(u.gauge.steps),P=f.selectAll("g.bg-bullet").data(C);P.enter().append("g").classed("bg-bullet",!0).append("rect"),P.select("rect").call(L).call(T),P.exit().remove();var I=f.selectAll("g.value-bullet").data([u.gauge.bar]);I.enter().append("g").classed("value-bullet",!0).append("rect"),I.select("rect").attr("height",M).attr("y",(A-M)/2).call(T),w(b)?I.select("rect").transition().duration(b.duration).ease(b.easing).each("end",(function(){_&&_()})).each("interrupt",(function(){_&&_()})).attr("width",Math.max(0,i.c2p(Math.min(u.gauge.axis.range[1],r[0].y)))):I.select("rect").attr("width","number"==typeof r[0].y?Math.max(0,i.c2p(Math.min(u.gauge.axis.range[1],r[0].y))):0);I.exit().remove();var O=r.filter((function(){return u.gauge.threshold.value||0===u.gauge.threshold.value})),z=f.selectAll("g.threshold-bullet").data(O);z.enter().append("g").classed("threshold-bullet",!0).append("line"),z.select("line").attr("x1",i.c2p(u.gauge.threshold.value)).attr("x2",i.c2p(u.gauge.threshold.value)).attr("y1",(1-u.gauge.threshold.thickness)/2*A).attr("y2",(1-(1-u.gauge.threshold.thickness)/2)*A).call(y.stroke,u.gauge.threshold.line.color).style("stroke-width",u.gauge.threshold.line.width),z.exit().remove();var D=f.selectAll("g.gauge-outline").data([g]);D.enter().append("g").classed("gauge-outline",!0).append("rect"),D.select("rect").call(L).call(T),D.exit().remove()}(t,0,e,{gauge:X,layer:Z,size:B,gaugeBg:C,gaugeOutline:P,transitionOpts:r,onComplete:g});var J=O.selectAll("text.title").data(e);J.exit().remove(),J.enter().append("text").classed("title",!0),J.attr("text-anchor",(function(){return R?x.right:x[I.title.align]})).text(I.title.text).call(f.font,I.title.font).call(p.convertToTspans,t),J.attr("transform",(function(){var t,e=B.l+B.w*b[I.title.align],r=h.titlePadding,n=f.bBox(J.node());if(z){if(D)if(I.gauge.axis.visible)t=f.bBox(W.node()).top-r-n.bottom;else t=B.t+B.h/2-U/2-n.bottom-r;R&&(t=E-(n.top+n.bottom)/2,e=B.l-h.bulletPadding*B.w)}else t=I._numbersTop-r-n.bottom;return l(e,t)}))}))}},{"../../components/color":366,"../../components/drawing":388,"../../constants/alignment":471,"../../lib":503,"../../lib/svg_text_utils":529,"../../plots/cartesian/axes":554,"../../plots/cartesian/axis_defaults":556,"../../plots/cartesian/layout_attributes":569,"../../plots/cartesian/position_defaults":572,"./constants":858,"@plotly/d3":58,"d3-interpolate":116}],862:[function(t,e,r){"use strict";var n=t("../../components/colorscale/attributes"),i=t("../../plots/cartesian/axis_format_attributes").axisHoverFormat,a=t("../../plots/template_attributes").hovertemplateAttrs,o=t("../mesh3d/attributes"),s=t("../../plots/attributes"),l=t("../../lib/extend").extendFlat,c=t("../../plot_api/edit_types").overrideAll;var u=e.exports=c(l({x:{valType:"data_array"},y:{valType:"data_array"},z:{valType:"data_array"},value:{valType:"data_array"},isomin:{valType:"number"},isomax:{valType:"number"},surface:{show:{valType:"boolean",dflt:!0},count:{valType:"integer",dflt:2,min:1},fill:{valType:"number",min:0,max:1,dflt:1},pattern:{valType:"flaglist",flags:["A","B","C","D","E"],extras:["all","odd","even"],dflt:"all"}},spaceframe:{show:{valType:"boolean",dflt:!1},fill:{valType:"number",min:0,max:1,dflt:.15}},slices:{x:{show:{valType:"boolean",dflt:!1},locations:{valType:"data_array",dflt:[]},fill:{valType:"number",min:0,max:1,dflt:1}},y:{show:{valType:"boolean",dflt:!1},locations:{valType:"data_array",dflt:[]},fill:{valType:"number",min:0,max:1,dflt:1}},z:{show:{valType:"boolean",dflt:!1},locations:{valType:"data_array",dflt:[]},fill:{valType:"number",min:0,max:1,dflt:1}}},caps:{x:{show:{valType:"boolean",dflt:!0},fill:{valType:"number",min:0,max:1,dflt:1}},y:{show:{valType:"boolean",dflt:!0},fill:{valType:"number",min:0,max:1,dflt:1}},z:{show:{valType:"boolean",dflt:!0},fill:{valType:"number",min:0,max:1,dflt:1}}},text:{valType:"string",dflt:"",arrayOk:!0},hovertext:{valType:"string",dflt:"",arrayOk:!0},hovertemplate:a(),xhoverformat:i("x"),yhoverformat:i("y"),zhoverformat:i("z"),valuehoverformat:i("value",1),showlegend:l({},s.showlegend,{dflt:!1})},n("",{colorAttr:"`value`",showScaleDflt:!0,editTypeOverride:"calc"}),{opacity:o.opacity,lightposition:o.lightposition,lighting:o.lighting,flatshading:o.flatshading,contour:o.contour,hoverinfo:l({},s.hoverinfo)}),"calc","nested");u.flatshading.dflt=!0,u.lighting.facenormalsepsilon.dflt=0,u.x.editType=u.y.editType=u.z.editType=u.value.editType="calc+clearAxisTypes",u.transforms=void 0},{"../../components/colorscale/attributes":373,"../../lib/extend":493,"../../plot_api/edit_types":536,"../../plots/attributes":550,"../../plots/cartesian/axis_format_attributes":557,"../../plots/template_attributes":633,"../mesh3d/attributes":867}],863:[function(t,e,r){"use strict";var n=t("../../components/colorscale/calc"),i=t("../streamtube/calc").processGrid,a=t("../streamtube/calc").filter;e.exports=function(t,e){e._len=Math.min(e.x.length,e.y.length,e.z.length,e.value.length),e._x=a(e.x,e._len),e._y=a(e.y,e._len),e._z=a(e.z,e._len),e._value=a(e.value,e._len);var r=i(e);e._gridFill=r.fill,e._Xs=r.Xs,e._Ys=r.Ys,e._Zs=r.Zs,e._len=r.len;for(var o=1/0,s=-1/0,l=0;l0;r--){var n=Math.min(e[r],e[r-1]),i=Math.max(e[r],e[r-1]);if(i>n&&n-1}function R(t,e){return null===t?e:t}function F(e,r,n){C();var i,a,o,l=[r],c=[n];if(s>=1)l=[r],c=[n];else if(s>0){var u=function(t,e){var r=t[0],n=t[1],i=t[2],a=function(t,e,r){for(var n=[],i=0;i-1?n[p]:L(d,m,v);h[p]=x>-1?x:I(d,m,v,R(e,y))}i=h[0],a=h[1],o=h[2],t._meshI.push(i),t._meshJ.push(a),t._meshK.push(o),++g}}function B(t,e,r,n){var i=t[3];in&&(i=n);for(var a=(t[3]-i)/(t[3]-e[3]+1e-9),o=[],s=0;s<4;s++)o[s]=(1-a)*t[s]+a*e[s];return o}function N(t,e,r){return t>=e&&t<=r}function j(t){var e=.001*(E-S);return t>=S-e&&t<=E+e}function U(e){for(var r=[],n=0;n<4;n++){var i=e[n];r.push([t._x[i],t._y[i],t._z[i],t._value[i]])}return r}function V(t,e,r,n,i,a){a||(a=1),r=[-1,-1,-1];var o=!1,s=[N(e[0][3],n,i),N(e[1][3],n,i),N(e[2][3],n,i)];if(!s[0]&&!s[1]&&!s[2])return!1;var l=function(t,e,r){return j(e[0][3])&&j(e[1][3])&&j(e[2][3])?(F(t,e,r),!0):a<3&&V(t,e,r,S,E,++a)};if(s[0]&&s[1]&&s[2])return l(t,e,r)||o;var c=!1;return[[0,1,2],[2,0,1],[1,2,0]].forEach((function(a){if(s[a[0]]&&s[a[1]]&&!s[a[2]]){var u=e[a[0]],f=e[a[1]],h=e[a[2]],p=B(h,u,n,i),d=B(h,f,n,i);o=l(t,[d,p,u],[-1,-1,r[a[0]]])||o,o=l(t,[u,f,d],[r[a[0]],r[a[1]],-1])||o,c=!0}})),c||[[0,1,2],[1,2,0],[2,0,1]].forEach((function(a){if(s[a[0]]&&!s[a[1]]&&!s[a[2]]){var u=e[a[0]],f=e[a[1]],h=e[a[2]],p=B(f,u,n,i),d=B(h,u,n,i);o=l(t,[d,p,u],[-1,-1,r[a[0]]])||o,c=!0}})),o}function H(t,e,r,n){var i=!1,a=U(e),o=[N(a[0][3],r,n),N(a[1][3],r,n),N(a[2][3],r,n),N(a[3][3],r,n)];if(!(o[0]||o[1]||o[2]||o[3]))return i;if(o[0]&&o[1]&&o[2]&&o[3])return m&&(i=function(t,e,r){var n=function(n,i,a){F(t,[e[n],e[i],e[a]],[r[n],r[i],r[a]])};n(0,1,2),n(3,0,1),n(2,3,0),n(1,2,3)}(t,a,e)||i),i;var s=!1;return[[0,1,2,3],[3,0,1,2],[2,3,0,1],[1,2,3,0]].forEach((function(l){if(o[l[0]]&&o[l[1]]&&o[l[2]]&&!o[l[3]]){var c=a[l[0]],u=a[l[1]],f=a[l[2]],h=a[l[3]];if(m)i=F(t,[c,u,f],[e[l[0]],e[l[1]],e[l[2]]])||i;else{var p=B(h,c,r,n),d=B(h,u,r,n),g=B(h,f,r,n);i=F(null,[p,d,g],[-1,-1,-1])||i}s=!0}})),s?i:([[0,1,2,3],[1,2,3,0],[2,3,0,1],[3,0,1,2],[0,2,3,1],[1,3,2,0]].forEach((function(l){if(o[l[0]]&&o[l[1]]&&!o[l[2]]&&!o[l[3]]){var c=a[l[0]],u=a[l[1]],f=a[l[2]],h=a[l[3]],p=B(f,c,r,n),d=B(f,u,r,n),g=B(h,u,r,n),v=B(h,c,r,n);m?(i=F(t,[c,v,p],[e[l[0]],-1,-1])||i,i=F(t,[u,d,g],[e[l[1]],-1,-1])||i):i=function(t,e,r){var n=function(n,i,a){F(t,[e[n],e[i],e[a]],[r[n],r[i],r[a]])};n(0,1,2),n(2,3,0)}(null,[p,d,g,v],[-1,-1,-1,-1])||i,s=!0}})),s||[[0,1,2,3],[1,2,3,0],[2,3,0,1],[3,0,1,2]].forEach((function(l){if(o[l[0]]&&!o[l[1]]&&!o[l[2]]&&!o[l[3]]){var c=a[l[0]],u=a[l[1]],f=a[l[2]],h=a[l[3]],p=B(u,c,r,n),d=B(f,c,r,n),g=B(h,c,r,n);m?(i=F(t,[c,p,d],[e[l[0]],-1,-1])||i,i=F(t,[c,d,g],[e[l[0]],-1,-1])||i,i=F(t,[c,g,p],[e[l[0]],-1,-1])||i):i=F(null,[p,d,g],[-1,-1,-1])||i,s=!0}})),i)}function q(t,e,r,n,i,a,o,s,l,c,u){var f=!1;return d&&(D(t,"A")&&(f=H(null,[e,r,n,a],c,u)||f),D(t,"B")&&(f=H(null,[r,n,i,l],c,u)||f),D(t,"C")&&(f=H(null,[r,a,o,l],c,u)||f),D(t,"D")&&(f=H(null,[n,a,s,l],c,u)||f),D(t,"E")&&(f=H(null,[r,n,a,l],c,u)||f)),m&&(f=H(t,[r,n,a,l],c,u)||f),f}function G(t,e,r,n,i,a,o,s){return[!0===s[0]||V(t,U([e,r,n]),[e,r,n],a,o),!0===s[1]||V(t,U([n,i,e]),[n,i,e],a,o)]}function Y(t,e,r,n,i,a,o,s,l){return s?G(t,e,r,i,n,a,o,l):G(t,r,i,n,e,a,o,l)}function W(t,e,r,n,i,a,o){var s,l,c,u,f=!1,h=function(){f=V(t,[s,l,c],[-1,-1,-1],i,a)||f,f=V(t,[c,u,s],[-1,-1,-1],i,a)||f},p=o[0],d=o[1],m=o[2];return p&&(s=O(U([k(e,r-0,n-0)])[0],U([k(e-1,r-0,n-0)])[0],p),l=O(U([k(e,r-0,n-1)])[0],U([k(e-1,r-0,n-1)])[0],p),c=O(U([k(e,r-1,n-1)])[0],U([k(e-1,r-1,n-1)])[0],p),u=O(U([k(e,r-1,n-0)])[0],U([k(e-1,r-1,n-0)])[0],p),h()),d&&(s=O(U([k(e-0,r,n-0)])[0],U([k(e-0,r-1,n-0)])[0],d),l=O(U([k(e-0,r,n-1)])[0],U([k(e-0,r-1,n-1)])[0],d),c=O(U([k(e-1,r,n-1)])[0],U([k(e-1,r-1,n-1)])[0],d),u=O(U([k(e-1,r,n-0)])[0],U([k(e-1,r-1,n-0)])[0],d),h()),m&&(s=O(U([k(e-0,r-0,n)])[0],U([k(e-0,r-0,n-1)])[0],m),l=O(U([k(e-0,r-1,n)])[0],U([k(e-0,r-1,n-1)])[0],m),c=O(U([k(e-1,r-1,n)])[0],U([k(e-1,r-1,n-1)])[0],m),u=O(U([k(e-1,r-0,n)])[0],U([k(e-1,r-0,n-1)])[0],m),h()),f}function X(t,e,r,n,i,a,o,s,l,c,u,f){var h=t;return f?(d&&"even"===t&&(h=null),q(h,e,r,n,i,a,o,s,l,c,u)):(d&&"odd"===t&&(h=null),q(h,l,s,o,a,i,n,r,e,c,u))}function Z(t,e,r,n,i){for(var a=[],o=0,s=0;sMath.abs(d-M)?[A,d]:[d,M];$(e,T[0],T[1])}}var L=[[Math.min(S,M),Math.max(S,M)],[Math.min(A,E),Math.max(A,E)]];["x","y","z"].forEach((function(e){for(var r=[],n=0;n0&&(u.push(p.id),"x"===e?f.push([p.distRatio,0,0]):"y"===e?f.push([0,p.distRatio,0]):f.push([0,0,p.distRatio]))}else c=nt(1,"x"===e?b-1:"y"===e?_-1:w-1);u.length>0&&(r[i]="x"===e?tt(null,u,a,o,f,r[i]):"y"===e?et(null,u,a,o,f,r[i]):rt(null,u,a,o,f,r[i]),i++),c.length>0&&(r[i]="x"===e?Z(null,c,a,o,r[i]):"y"===e?J(null,c,a,o,r[i]):K(null,c,a,o,r[i]),i++)}var d=t.caps[e];d.show&&d.fill&&(z(d.fill),r[i]="x"===e?Z(null,[0,b-1],a,o,r[i]):"y"===e?J(null,[0,_-1],a,o,r[i]):K(null,[0,w-1],a,o,r[i]),i++)}})),0===g&&P(),t._meshX=n,t._meshY=i,t._meshZ=a,t._meshIntensity=o,t._Xs=v,t._Ys=y,t._Zs=x}(),t}e.exports={findNearestOnAxis:l,generateIsoMeshes:h,createIsosurfaceTrace:function(t,e){var r=t.glplot.gl,i=n({gl:r}),a=new c(t,i,e.uid);return i._trace=a,a.update(e),t.glplot.add(i),a}}},{"../../../stackgl_modules":1124,"../../components/colorscale":378,"../../lib/gl_format_color":499,"../../lib/str2rgbarray":528,"../../plots/gl3d/zip3":609}],865:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("../../registry"),a=t("./attributes"),o=t("../../components/colorscale/defaults");function s(t,e,r,n,a){var s=a("isomin"),l=a("isomax");null!=l&&null!=s&&s>l&&(e.isomin=null,e.isomax=null);var c=a("x"),u=a("y"),f=a("z"),h=a("value");c&&c.length&&u&&u.length&&f&&f.length&&h&&h.length?(i.getComponentMethod("calendars","handleTraceDefaults")(t,e,["x","y","z"],n),a("valuehoverformat"),["x","y","z"].forEach((function(t){a(t+"hoverformat");var e="caps."+t;a(e+".show")&&a(e+".fill");var r="slices."+t;a(r+".show")&&(a(r+".fill"),a(r+".locations"))})),a("spaceframe.show")&&a("spaceframe.fill"),a("surface.show")&&(a("surface.count"),a("surface.fill"),a("surface.pattern")),a("contour.show")&&(a("contour.color"),a("contour.width")),["text","hovertext","hovertemplate","lighting.ambient","lighting.diffuse","lighting.specular","lighting.roughness","lighting.fresnel","lighting.vertexnormalsepsilon","lighting.facenormalsepsilon","lightposition.x","lightposition.y","lightposition.z","flatshading","opacity"].forEach((function(t){a(t)})),o(t,e,n,a,{prefix:"",cLetter:"c"}),e._length=null):e.visible=!1}e.exports={supplyDefaults:function(t,e,r,i){s(t,e,r,i,(function(r,i){return n.coerce(t,e,a,r,i)}))},supplyIsoDefaults:s}},{"../../components/colorscale/defaults":376,"../../lib":503,"../../registry":638,"./attributes":862}],866:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),supplyDefaults:t("./defaults").supplyDefaults,calc:t("./calc"),colorbar:{min:"cmin",max:"cmax"},plot:t("./convert").createIsosurfaceTrace,moduleType:"trace",name:"isosurface",basePlotModule:t("../../plots/gl3d"),categories:["gl3d","showLegend"],meta:{}}},{"../../plots/gl3d":598,"./attributes":862,"./calc":863,"./convert":864,"./defaults":865}],867:[function(t,e,r){"use strict";var n=t("../../components/colorscale/attributes"),i=t("../../plots/cartesian/axis_format_attributes").axisHoverFormat,a=t("../../plots/template_attributes").hovertemplateAttrs,o=t("../surface/attributes"),s=t("../../plots/attributes"),l=t("../../lib/extend").extendFlat;e.exports=l({x:{valType:"data_array",editType:"calc+clearAxisTypes"},y:{valType:"data_array",editType:"calc+clearAxisTypes"},z:{valType:"data_array",editType:"calc+clearAxisTypes"},i:{valType:"data_array",editType:"calc"},j:{valType:"data_array",editType:"calc"},k:{valType:"data_array",editType:"calc"},text:{valType:"string",dflt:"",arrayOk:!0,editType:"calc"},hovertext:{valType:"string",dflt:"",arrayOk:!0,editType:"calc"},hovertemplate:a({editType:"calc"}),xhoverformat:i("x"),yhoverformat:i("y"),zhoverformat:i("z"),delaunayaxis:{valType:"enumerated",values:["x","y","z"],dflt:"z",editType:"calc"},alphahull:{valType:"number",dflt:-1,editType:"calc"},intensity:{valType:"data_array",editType:"calc"},intensitymode:{valType:"enumerated",values:["vertex","cell"],dflt:"vertex",editType:"calc"},color:{valType:"color",editType:"calc"},vertexcolor:{valType:"data_array",editType:"calc"},facecolor:{valType:"data_array",editType:"calc"},transforms:void 0},n("",{colorAttr:"`intensity`",showScaleDflt:!0,editTypeOverride:"calc"}),{opacity:o.opacity,flatshading:{valType:"boolean",dflt:!1,editType:"calc"},contour:{show:l({},o.contours.x.show,{}),color:o.contours.x.color,width:o.contours.x.width,editType:"calc"},lightposition:{x:l({},o.lightposition.x,{dflt:1e5}),y:l({},o.lightposition.y,{dflt:1e5}),z:l({},o.lightposition.z,{dflt:0}),editType:"calc"},lighting:l({vertexnormalsepsilon:{valType:"number",min:0,max:1,dflt:1e-12,editType:"calc"},facenormalsepsilon:{valType:"number",min:0,max:1,dflt:1e-6,editType:"calc"},editType:"calc"},o.lighting),hoverinfo:l({},s.hoverinfo,{editType:"calc"}),showlegend:l({},s.showlegend,{dflt:!1})})},{"../../components/colorscale/attributes":373,"../../lib/extend":493,"../../plots/attributes":550,"../../plots/cartesian/axis_format_attributes":557,"../../plots/template_attributes":633,"../surface/attributes":1061}],868:[function(t,e,r){"use strict";var n=t("../../components/colorscale/calc");e.exports=function(t,e){e.intensity&&n(t,e,{vals:e.intensity,containerStr:"",cLetter:"c"})}},{"../../components/colorscale/calc":374}],869:[function(t,e,r){"use strict";var n=t("../../../stackgl_modules").gl_mesh3d,i=t("../../../stackgl_modules").delaunay_triangulate,a=t("../../../stackgl_modules").alpha_shape,o=t("../../../stackgl_modules").convex_hull,s=t("../../lib/gl_format_color").parseColorScale,l=t("../../lib/str2rgbarray"),c=t("../../components/colorscale").extractOpts,u=t("../../plots/gl3d/zip3");function f(t,e,r){this.scene=t,this.uid=r,this.mesh=e,this.name="",this.color="#fff",this.data=null,this.showContour=!1}var h=f.prototype;function p(t){for(var e=[],r=t.length,n=0;n=e-.5)return!1;return!0}h.handlePick=function(t){if(t.object===this.mesh){var e=t.index=t.data.index;t.data._cellCenter?t.traceCoordinate=t.data.dataCoordinate:t.traceCoordinate=[this.data.x[e],this.data.y[e],this.data.z[e]];var r=this.data.hovertext||this.data.text;return Array.isArray(r)&&void 0!==r[e]?t.textLabel=r[e]:r&&(t.textLabel=r),!0}},h.update=function(t){var e=this.scene,r=e.fullSceneLayout;this.data=t;var n,f=t.x.length,h=u(d(r.xaxis,t.x,e.dataScale[0],t.xcalendar),d(r.yaxis,t.y,e.dataScale[1],t.ycalendar),d(r.zaxis,t.z,e.dataScale[2],t.zcalendar));if(t.i&&t.j&&t.k){if(t.i.length!==t.j.length||t.j.length!==t.k.length||!g(t.i,f)||!g(t.j,f)||!g(t.k,f))return;n=u(m(t.i),m(t.j),m(t.k))}else n=0===t.alphahull?o(h):t.alphahull>0?a(t.alphahull,h):function(t,e){for(var r=["x","y","z"].indexOf(t),n=[],a=e.length,o=0;ov):g=A>w,v=A;var M=c(w,T,k,A);M.pos=_,M.yc=(w+A)/2,M.i=b,M.dir=g?"increasing":"decreasing",M.x=M.pos,M.y=[k,T],y&&(M.orig_p=r[b]),d&&(M.tx=e.text[b]),m&&(M.htx=e.hovertext[b]),x.push(M)}else x.push({pos:_,empty:!0})}return e._extremes[l._id]=a.findExtremes(l,n.concat(h,f),{padded:!0}),x.length&&(x[0].t={labels:{open:i(t,"open:")+" ",high:i(t,"high:")+" ",low:i(t,"low:")+" ",close:i(t,"close:")+" "}}),x}e.exports={calc:function(t,e){var r=a.getFromId(t,e.xaxis),i=a.getFromId(t,e.yaxis),s=function(t,e,r){var i=r._minDiff;if(!i){var a,s=t._fullData,l=[];for(i=1/0,a=0;a"+c.labels[x]+n.hoverLabelText(s,b,l.yhoverformat):((y=i.extendFlat({},h)).y0=y.y1=_,y.yLabelVal=b,y.yLabel=c.labels[x]+n.hoverLabelText(s,b,l.yhoverformat),y.name="",f.push(y),g[b]=y)}return f}function h(t,e,r,i){var a=t.cd,o=t.ya,l=a[0].trace,f=a[0].t,h=u(t,e,r,i);if(!h)return[];var p=a[h.index],d=h.index=p.i,m=p.dir;function g(t){return f.labels[t]+n.hoverLabelText(o,l[t][d],l.yhoverformat)}var v=p.hi||l.hoverinfo,y=v.split("+"),x="all"===v,b=x||-1!==y.indexOf("y"),_=x||-1!==y.indexOf("text"),w=b?[g("open"),g("high"),g("low"),g("close")+" "+c[m]]:[];return _&&s(p,l,w),h.extraText=w.join("
"),h.y0=h.y1=o.c2p(p.yc,!0),[h]}e.exports={hoverPoints:function(t,e,r,n){return t.cd[0].trace.hoverlabel.split?f(t,e,r,n):h(t,e,r,n)},hoverSplit:f,hoverOnPoints:h}},{"../../components/color":366,"../../components/fx":406,"../../constants/delta.js":473,"../../lib":503,"../../plots/cartesian/axes":554}],876:[function(t,e,r){"use strict";e.exports={moduleType:"trace",name:"ohlc",basePlotModule:t("../../plots/cartesian"),categories:["cartesian","svg","showLegend"],meta:{},attributes:t("./attributes"),supplyDefaults:t("./defaults"),calc:t("./calc").calc,plot:t("./plot"),style:t("./style"),hoverPoints:t("./hover").hoverPoints,selectPoints:t("./select")}},{"../../plots/cartesian":568,"./attributes":872,"./calc":873,"./defaults":874,"./hover":875,"./plot":878,"./select":879,"./style":880}],877:[function(t,e,r){"use strict";var n=t("../../registry"),i=t("../../lib");e.exports=function(t,e,r,a){var o=r("x"),s=r("open"),l=r("high"),c=r("low"),u=r("close");if(r("hoverlabel.split"),n.getComponentMethod("calendars","handleTraceDefaults")(t,e,["x"],a),s&&l&&c&&u){var f=Math.min(s.length,l.length,c.length,u.length);return o&&(f=Math.min(f,i.minRowLength(o))),e._length=f,f}}},{"../../lib":503,"../../registry":638}],878:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../lib");e.exports=function(t,e,r,a){var o=e.yaxis,s=e.xaxis,l=!!s.rangebreaks;i.makeTraceGroups(a,r,"trace ohlc").each((function(t){var e=n.select(this),r=t[0],a=r.t;if(!0!==r.trace.visible||a.empty)e.remove();else{var c=a.tickLen,u=e.selectAll("path").data(i.identity);u.enter().append("path"),u.exit().remove(),u.attr("d",(function(t){if(t.empty)return"M0,0Z";var e=s.c2p(t.pos-c,!0),r=s.c2p(t.pos+c,!0),n=l?(e+r)/2:s.c2p(t.pos,!0);return"M"+e+","+o.c2p(t.o,!0)+"H"+n+"M"+n+","+o.c2p(t.h,!0)+"V"+o.c2p(t.l,!0)+"M"+r+","+o.c2p(t.c,!0)+"H"+n}))}}))}},{"../../lib":503,"@plotly/d3":58}],879:[function(t,e,r){"use strict";e.exports=function(t,e){var r,n=t.cd,i=t.xaxis,a=t.yaxis,o=[],s=n[0].t.bPos||0;if(!1===e)for(r=0;r=t.length)return!1;if(void 0!==e[t[r]])return!1;e[t[r]]=!0}return!0}(t.map((function(t){return t.displayindex}))))for(e=0;e0;c&&(o="array");var u=r("categoryorder",o);"array"===u?(r("categoryarray"),r("ticktext")):(delete t.categoryarray,delete t.ticktext),c||"array"!==u||(e.categoryorder="trace")}}e.exports=function(t,e,r,f){function h(r,i){return n.coerce(t,e,l,r,i)}var p=s(t,e,{name:"dimensions",handleItemDefaults:u}),d=function(t,e,r,o,s){s("line.shape"),s("line.hovertemplate");var l=s("line.color",o.colorway[0]);if(i(t,"line")&&n.isArrayOrTypedArray(l)){if(l.length)return s("line.colorscale"),a(t,e,o,s,{prefix:"line.",cLetter:"c"}),l.length;e.line.color=r}return 1/0}(t,e,r,f,h);o(e,f,h),Array.isArray(p)&&p.length||(e.visible=!1),c(e,p,"values",d),h("hoveron"),h("hovertemplate"),h("arrangement"),h("bundlecolors"),h("sortpaths"),h("counts");var m={family:f.font.family,size:Math.round(f.font.size),color:f.font.color};n.coerceFont(h,"labelfont",m);var g={family:f.font.family,size:Math.round(f.font.size/1.2),color:f.font.color};n.coerceFont(h,"tickfont",g)}},{"../../components/colorscale/defaults":376,"../../components/colorscale/helpers":377,"../../lib":503,"../../plots/array_container_defaults":549,"../../plots/domain":584,"../parcoords/merge_length":898,"./attributes":881}],885:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),supplyDefaults:t("./defaults"),calc:t("./calc"),plot:t("./plot"),colorbar:{container:"line",min:"cmin",max:"cmax"},moduleType:"trace",name:"parcats",basePlotModule:t("./base_plot"),categories:["noOpacity"],meta:{}}},{"./attributes":881,"./base_plot":882,"./calc":883,"./defaults":884,"./plot":887}],886:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("d3-interpolate").interpolateNumber,a=t("../../plot_api/plot_api"),o=t("../../components/fx"),s=t("../../lib"),l=s.strTranslate,c=t("../../components/drawing"),u=t("tinycolor2"),f=t("../../lib/svg_text_utils");function h(t,e,r,i){var a=t.map(F.bind(0,e,r)),o=i.selectAll("g.parcatslayer").data([null]);o.enter().append("g").attr("class","parcatslayer").style("pointer-events","all");var u=o.selectAll("g.trace.parcats").data(a,p),h=u.enter().append("g").attr("class","trace parcats");u.attr("transform",(function(t){return l(t.x,t.y)})),h.append("g").attr("class","paths");var y=u.select("g.paths").selectAll("path.path").data((function(t){return t.paths}),p);y.attr("fill",(function(t){return t.model.color}));var x=y.enter().append("path").attr("class","path").attr("stroke-opacity",0).attr("fill",(function(t){return t.model.color})).attr("fill-opacity",0);_(x),y.attr("d",(function(t){return t.svgD})),x.empty()||y.sort(m),y.exit().remove(),y.on("mouseover",g).on("mouseout",v).on("click",b),h.append("g").attr("class","dimensions");var w=u.select("g.dimensions").selectAll("g.dimension").data((function(t){return t.dimensions}),p);w.enter().append("g").attr("class","dimension"),w.attr("transform",(function(t){return l(t.x,0)})),w.exit().remove();var A=w.selectAll("g.category").data((function(t){return t.categories}),p),M=A.enter().append("g").attr("class","category");A.attr("transform",(function(t){return l(0,t.y)})),M.append("rect").attr("class","catrect").attr("pointer-events","none"),A.select("rect.catrect").attr("fill","none").attr("width",(function(t){return t.width})).attr("height",(function(t){return t.height})),T(M);var S=A.selectAll("rect.bandrect").data((function(t){return t.bands}),p);S.each((function(){s.raiseToTop(this)})),S.attr("fill",(function(t){return t.color}));var E=S.enter().append("rect").attr("class","bandrect").attr("stroke-opacity",0).attr("fill",(function(t){return t.color})).attr("fill-opacity",0);S.attr("fill",(function(t){return t.color})).attr("width",(function(t){return t.width})).attr("height",(function(t){return t.height})).attr("y",(function(t){return t.y})).attr("cursor",(function(t){return"fixed"===t.parcatsViewModel.arrangement?"default":"perpendicular"===t.parcatsViewModel.arrangement?"ns-resize":"move"})),k(E),S.exit().remove(),M.append("text").attr("class","catlabel").attr("pointer-events","none");var z=e._fullLayout.paper_bgcolor;A.select("text.catlabel").attr("text-anchor",(function(t){return d(t)?"start":"end"})).attr("alignment-baseline","middle").style("text-shadow",f.makeTextShadow(z)).style("fill","rgb(0, 0, 0)").attr("x",(function(t){return d(t)?t.width+5:-5})).attr("y",(function(t){return t.height/2})).text((function(t){return t.model.categoryLabel})).each((function(t){c.font(n.select(this),t.parcatsViewModel.categorylabelfont),f.convertToTspans(n.select(this),e)})),M.append("text").attr("class","dimlabel"),A.select("text.dimlabel").attr("text-anchor","middle").attr("alignment-baseline","baseline").attr("cursor",(function(t){return"fixed"===t.parcatsViewModel.arrangement?"default":"ew-resize"})).attr("x",(function(t){return t.width/2})).attr("y",-5).text((function(t,e){return 0===e?t.parcatsViewModel.model.dimensions[t.model.dimensionInd].dimensionLabel:null})).each((function(t){c.font(n.select(this),t.parcatsViewModel.labelfont)})),A.selectAll("rect.bandrect").on("mouseover",L).on("mouseout",C),A.exit().remove(),w.call(n.behavior.drag().origin((function(t){return{x:t.x,y:0}})).on("dragstart",P).on("drag",I).on("dragend",O)),u.each((function(t){t.traceSelection=n.select(this),t.pathSelection=n.select(this).selectAll("g.paths").selectAll("path.path"),t.dimensionSelection=n.select(this).selectAll("g.dimensions").selectAll("g.dimension")})),u.exit().remove()}function p(t){return t.key}function d(t){var e=t.parcatsViewModel.dimensions.length,r=t.parcatsViewModel.dimensions[e-1].model.dimensionInd;return t.model.dimensionInd===r}function m(t,e){return t.model.rawColor>e.model.rawColor?1:t.model.rawColor"),L=n.mouse(f)[0];o.loneHover({trace:h,x:b-d.left+m.left,y:_-d.top+m.top,text:E,color:t.model.color,borderColor:"black",fontFamily:'Monaco, "Courier New", monospace',fontSize:10,fontColor:T,idealAlign:L1&&h.displayInd===f.dimensions.length-1?(i=c.left,a="left"):(i=c.left+c.width,a="right");var m=u.model.count,g=u.model.categoryLabel,v=m/u.parcatsViewModel.model.count,y={countLabel:m,categoryLabel:g,probabilityLabel:v.toFixed(3)},x=[];-1!==u.parcatsViewModel.hoverinfoItems.indexOf("count")&&x.push(["Count:",y.countLabel].join(" ")),-1!==u.parcatsViewModel.hoverinfoItems.indexOf("probability")&&x.push(["P("+y.categoryLabel+"):",y.probabilityLabel].join(" "));var b=x.join("
");return{trace:p,x:o*(i-e.left),y:s*(d-e.top),text:b,color:"lightgray",borderColor:"black",fontFamily:'Monaco, "Courier New", monospace',fontSize:12,fontColor:"black",idealAlign:a,hovertemplate:p.hovertemplate,hovertemplateLabels:y,eventData:[{data:p._input,fullData:p,count:m,category:g,probability:v}]}}function L(t){if(!t.parcatsViewModel.dragDimension&&-1===t.parcatsViewModel.hoverinfoItems.indexOf("skip")){if(n.mouse(this)[1]<-1)return;var e,r=t.parcatsViewModel.graphDiv,i=r._fullLayout,a=i._paperdiv.node().getBoundingClientRect(),l=t.parcatsViewModel.hoveron;if("color"===l?(!function(t){var e=n.select(t).datum(),r=A(e);w(r),r.each((function(){s.raiseToTop(this)})),n.select(t.parentNode).selectAll("rect.bandrect").filter((function(t){return t.color===e.color})).each((function(){s.raiseToTop(this),n.select(this).attr("stroke","black").attr("stroke-width",1.5)}))}(this),S(this,"plotly_hover",n.event)):(!function(t){n.select(t.parentNode).selectAll("rect.bandrect").each((function(t){var e=A(t);w(e),e.each((function(){s.raiseToTop(this)}))})),n.select(t.parentNode).select("rect.catrect").attr("stroke","black").attr("stroke-width",2.5)}(this),M(this,"plotly_hover",n.event)),-1===t.parcatsViewModel.hoverinfoItems.indexOf("none"))"category"===l?e=E(r,a,this):"color"===l?e=function(t,e,r){t._fullLayout._calcInverseTransform(t);var i,a,o=t._fullLayout._invScaleX,s=t._fullLayout._invScaleY,l=r.getBoundingClientRect(),c=n.select(r).datum(),f=c.categoryViewModel,h=f.parcatsViewModel,p=h.model.dimensions[f.model.dimensionInd],d=h.trace,m=l.y+l.height/2;h.dimensions.length>1&&p.displayInd===h.dimensions.length-1?(i=l.left,a="left"):(i=l.left+l.width,a="right");var g=f.model.categoryLabel,v=c.parcatsViewModel.model.count,y=0;c.categoryViewModel.bands.forEach((function(t){t.color===c.color&&(y+=t.count)}));var x=f.model.count,b=0;h.pathSelection.each((function(t){t.model.color===c.color&&(b+=t.model.count)}));var _=y/v,w=y/b,T=y/x,k={countLabel:v,categoryLabel:g,probabilityLabel:_.toFixed(3)},A=[];-1!==f.parcatsViewModel.hoverinfoItems.indexOf("count")&&A.push(["Count:",k.countLabel].join(" ")),-1!==f.parcatsViewModel.hoverinfoItems.indexOf("probability")&&(A.push("P(color \u2229 "+g+"): "+k.probabilityLabel),A.push("P("+g+" | color): "+w.toFixed(3)),A.push("P(color | "+g+"): "+T.toFixed(3)));var M=A.join("
"),S=u.mostReadable(c.color,["black","white"]);return{trace:d,x:o*(i-e.left),y:s*(m-e.top),text:M,color:c.color,borderColor:"black",fontFamily:'Monaco, "Courier New", monospace',fontColor:S,fontSize:10,idealAlign:a,hovertemplate:d.hovertemplate,hovertemplateLabels:k,eventData:[{data:d._input,fullData:d,category:g,count:v,probability:_,categorycount:x,colorcount:b,bandcolorcount:y}]}}(r,a,this):"dimension"===l&&(e=function(t,e,r){var i=[];return n.select(r.parentNode.parentNode).selectAll("g.category").select("rect.catrect").each((function(){i.push(E(t,e,this))})),i}(r,a,this)),e&&o.loneHover(e,{container:i._hoverlayer.node(),outerContainer:i._paper.node(),gd:r})}}function C(t){var e=t.parcatsViewModel;if(!e.dragDimension&&(_(e.pathSelection),T(e.dimensionSelection.selectAll("g.category")),k(e.dimensionSelection.selectAll("g.category").selectAll("rect.bandrect")),o.loneUnhover(e.graphDiv._fullLayout._hoverlayer.node()),e.pathSelection.sort(m),-1===e.hoverinfoItems.indexOf("skip"))){"color"===t.parcatsViewModel.hoveron?S(this,"plotly_unhover",n.event):M(this,"plotly_unhover",n.event)}}function P(t){"fixed"!==t.parcatsViewModel.arrangement&&(t.dragDimensionDisplayInd=t.model.displayInd,t.initialDragDimensionDisplayInds=t.parcatsViewModel.model.dimensions.map((function(t){return t.displayInd})),t.dragHasMoved=!1,t.dragCategoryDisplayInd=null,n.select(this).selectAll("g.category").select("rect.catrect").each((function(e){var r=n.mouse(this)[0],i=n.mouse(this)[1];-2<=r&&r<=e.width+2&&-2<=i&&i<=e.height+2&&(t.dragCategoryDisplayInd=e.model.displayInd,t.initialDragCategoryDisplayInds=t.model.categories.map((function(t){return t.displayInd})),e.model.dragY=e.y,s.raiseToTop(this.parentNode),n.select(this.parentNode).selectAll("rect.bandrect").each((function(e){e.yf.y+f.height/2&&(o.model.displayInd=f.model.displayInd,f.model.displayInd=l),t.dragCategoryDisplayInd=o.model.displayInd}if(null===t.dragCategoryDisplayInd||"freeform"===t.parcatsViewModel.arrangement){a.model.dragX=n.event.x;var h=t.parcatsViewModel.dimensions[r],p=t.parcatsViewModel.dimensions[i];void 0!==h&&a.model.dragXp.x&&(a.model.displayInd=p.model.displayInd,p.model.displayInd=t.dragDimensionDisplayInd),t.dragDimensionDisplayInd=a.model.displayInd}j(t.parcatsViewModel),N(t.parcatsViewModel),R(t.parcatsViewModel),D(t.parcatsViewModel)}}function O(t){if("fixed"!==t.parcatsViewModel.arrangement&&null!==t.dragDimensionDisplayInd){n.select(this).selectAll("text").attr("font-weight","normal");var e={},r=z(t.parcatsViewModel),i=t.parcatsViewModel.model.dimensions.map((function(t){return t.displayInd})),o=t.initialDragDimensionDisplayInds.some((function(t,e){return t!==i[e]}));o&&i.forEach((function(r,n){var i=t.parcatsViewModel.model.dimensions[n].containerInd;e["dimensions["+i+"].displayindex"]=r}));var s=!1;if(null!==t.dragCategoryDisplayInd){var l=t.model.categories.map((function(t){return t.displayInd}));if(s=t.initialDragCategoryDisplayInds.some((function(t,e){return t!==l[e]}))){var c=t.model.categories.slice().sort((function(t,e){return t.displayInd-e.displayInd})),u=c.map((function(t){return t.categoryValue})),f=c.map((function(t){return t.categoryLabel}));e["dimensions["+t.model.containerInd+"].categoryarray"]=[u],e["dimensions["+t.model.containerInd+"].ticktext"]=[f],e["dimensions["+t.model.containerInd+"].categoryorder"]="array"}}if(-1===t.parcatsViewModel.hoverinfoItems.indexOf("skip")&&!t.dragHasMoved&&t.potentialClickBand&&("color"===t.parcatsViewModel.hoveron?S(t.potentialClickBand,"plotly_click",n.event.sourceEvent):M(t.potentialClickBand,"plotly_click",n.event.sourceEvent)),t.model.dragX=null,null!==t.dragCategoryDisplayInd)t.parcatsViewModel.dimensions[t.dragDimensionDisplayInd].categories[t.dragCategoryDisplayInd].model.dragY=null,t.dragCategoryDisplayInd=null;t.dragDimensionDisplayInd=null,t.parcatsViewModel.dragDimension=null,t.dragHasMoved=null,t.potentialClickBand=null,j(t.parcatsViewModel),N(t.parcatsViewModel),n.transition().duration(300).ease("cubic-in-out").each((function(){R(t.parcatsViewModel,!0),D(t.parcatsViewModel,!0)})).each("end",(function(){(o||s)&&a.restyle(t.parcatsViewModel.graphDiv,e,[r])}))}}function z(t){for(var e,r=t.graphDiv._fullData,n=0;n=0;s--)u+="C"+c[s]+","+(e[s+1]+n)+" "+l[s]+","+(e[s]+n)+" "+(t[s]+r[s])+","+(e[s]+n),u+="l-"+r[s]+",0 ";return u+="Z"}function N(t){var e=t.dimensions,r=t.model,n=e.map((function(t){return t.categories.map((function(t){return t.y}))})),i=t.model.dimensions.map((function(t){return t.categories.map((function(t){return t.displayInd}))})),a=t.model.dimensions.map((function(t){return t.displayInd})),o=t.dimensions.map((function(t){return t.model.dimensionInd})),s=e.map((function(t){return t.x})),l=e.map((function(t){return t.width})),c=[];for(var u in r.paths)r.paths.hasOwnProperty(u)&&c.push(r.paths[u]);function f(t){var e=t.categoryInds.map((function(t,e){return i[e][t]}));return o.map((function(t){return e[t]}))}c.sort((function(e,r){var n=f(e),i=f(r);return"backward"===t.sortpaths&&(n.reverse(),i.reverse()),n.push(e.valueInds[0]),i.push(r.valueInds[0]),t.bundlecolors&&(n.unshift(e.rawColor),i.unshift(r.rawColor)),ni?1:0}));for(var h=new Array(c.length),p=e[0].model.count,d=e[0].categories.map((function(t){return t.height})).reduce((function(t,e){return t+e})),m=0;m0?d*(v.count/p):0;for(var y,x=new Array(n.length),b=0;b1?(t.width-80-16)/(n-1):0)*i;var a,o,s,l,c,u=[],f=t.model.maxCats,h=e.categories.length,p=e.count,d=t.height-8*(f-1),m=8*(f-h)/2,g=e.categories.map((function(t){return{displayInd:t.displayInd,categoryInd:t.categoryInd}}));for(g.sort((function(t,e){return t.displayInd-e.displayInd})),c=0;c0?o.count/p*d:0,s={key:o.valueInds[0],model:o,width:16,height:a,y:null!==o.dragY?o.dragY:m,bands:[],parcatsViewModel:t},m=m+a+8,u.push(s);return{key:e.dimensionInd,x:null!==e.dragX?e.dragX:r,y:0,width:16,model:e,categories:u,parcatsViewModel:t,dragCategoryDisplayInd:null,dragDimensionDisplayInd:null,initialDragDimensionDisplayInds:null,initialDragCategoryDisplayInds:null,dragHasMoved:null,potentialClickBand:null}}e.exports=function(t,e,r,n){h(r,t,n,e)}},{"../../components/drawing":388,"../../components/fx":406,"../../lib":503,"../../lib/svg_text_utils":529,"../../plot_api/plot_api":540,"@plotly/d3":58,"d3-interpolate":116,tinycolor2:312}],887:[function(t,e,r){"use strict";var n=t("./parcats");e.exports=function(t,e,r,i){var a=t._fullLayout,o=a._paper,s=a._size;n(t,o,e,{width:s.w,height:s.h,margin:{t:s.t,r:s.r,b:s.b,l:s.l}},r,i)}},{"./parcats":886}],888:[function(t,e,r){"use strict";var n=t("../../components/colorscale/attributes"),i=t("../../plots/cartesian/layout_attributes"),a=t("../../plots/font_attributes"),o=t("../../plots/domain").attributes,s=t("../../lib/extend").extendFlat,l=t("../../plot_api/plot_template").templatedArray;e.exports={domain:o({name:"parcoords",trace:!0,editType:"plot"}),labelangle:{valType:"angle",dflt:0,editType:"plot"},labelside:{valType:"enumerated",values:["top","bottom"],dflt:"top",editType:"plot"},labelfont:a({editType:"plot"}),tickfont:a({editType:"plot"}),rangefont:a({editType:"plot"}),dimensions:l("dimension",{label:{valType:"string",editType:"plot"},tickvals:s({},i.tickvals,{editType:"plot"}),ticktext:s({},i.ticktext,{editType:"plot"}),tickformat:s({},i.tickformat,{editType:"plot"}),visible:{valType:"boolean",dflt:!0,editType:"plot"},range:{valType:"info_array",items:[{valType:"number",editType:"plot"},{valType:"number",editType:"plot"}],editType:"plot"},constraintrange:{valType:"info_array",freeLength:!0,dimensions:"1-2",items:[{valType:"any",editType:"plot"},{valType:"any",editType:"plot"}],editType:"plot"},multiselect:{valType:"boolean",dflt:!0,editType:"plot"},values:{valType:"data_array",editType:"calc"},editType:"calc"}),line:s({editType:"calc"},n("line",{colorscaleDflt:"Viridis",autoColorDflt:!1,editTypeOverride:"calc"}))}},{"../../components/colorscale/attributes":373,"../../lib/extend":493,"../../plot_api/plot_template":543,"../../plots/cartesian/layout_attributes":569,"../../plots/domain":584,"../../plots/font_attributes":585}],889:[function(t,e,r){"use strict";var n=t("./constants"),i=t("@plotly/d3"),a=t("../../lib/gup").keyFun,o=t("../../lib/gup").repeat,s=t("../../lib").sorterAsc,l=t("../../lib").strTranslate,c=n.bar.snapRatio;function u(t,e){return t*(1-c)+e*c}var f=n.bar.snapClose;function h(t,e){return t*(1-f)+e*f}function p(t,e,r,n){if(function(t,e){for(var r=0;r=e[r][0]&&t<=e[r][1])return!0;return!1}(r,n))return r;var i=t?-1:1,a=0,o=e.length-1;if(i<0){var s=a;a=o,o=s}for(var l=e[a],c=l,f=a;i*fe){h=r;break}}if(a=u,isNaN(a)&&(a=isNaN(f)||isNaN(h)?isNaN(f)?h:f:e-c[f][1]t[1]+r||e=.9*t[1]+.1*t[0]?"n":e<=.9*t[0]+.1*t[1]?"s":"ns"}(d,e);m&&(o.interval=l[a],o.intervalPix=d,o.region=m)}}if(t.ordinal&&!o.region){var g=t.unitTickvals,y=t.unitToPaddedPx.invert(e);for(r=0;r=x[0]&&y<=x[1]){o.clickableOrdinalRange=x;break}}}return o}function w(t,e){i.event.sourceEvent.stopPropagation();var r=e.height-i.mouse(t)[1]-2*n.verticalPadding,a=e.brush.svgBrush;a.wasDragged=!0,a._dragging=!0,a.grabbingBar?a.newExtent=[r-a.grabPoint,r+a.barLength-a.grabPoint].map(e.unitToPaddedPx.invert):a.newExtent=[a.startExtent,e.unitToPaddedPx.invert(r)].sort(s),e.brush.filterSpecified=!0,a.extent=a.stayingIntervals.concat([a.newExtent]),a.brushCallback(e),b(t.parentNode)}function T(t,e){var r=_(e,e.height-i.mouse(t)[1]-2*n.verticalPadding),a="crosshair";r.clickableOrdinalRange?a="pointer":r.region&&(a=r.region+"-resize"),i.select(document.body).style("cursor",a)}function k(t){t.on("mousemove",(function(t){i.event.preventDefault(),t.parent.inBrushDrag||T(this,t)})).on("mouseleave",(function(t){t.parent.inBrushDrag||y()})).call(i.behavior.drag().on("dragstart",(function(t){!function(t,e){i.event.sourceEvent.stopPropagation();var r=e.height-i.mouse(t)[1]-2*n.verticalPadding,a=e.unitToPaddedPx.invert(r),o=e.brush,s=_(e,r),l=s.interval,c=o.svgBrush;if(c.wasDragged=!1,c.grabbingBar="ns"===s.region,c.grabbingBar){var u=l.map(e.unitToPaddedPx);c.grabPoint=r-u[0]-n.verticalPadding,c.barLength=u[1]-u[0]}c.clickableOrdinalRange=s.clickableOrdinalRange,c.stayingIntervals=e.multiselect&&o.filterSpecified?o.filter.getConsolidated():[],l&&(c.stayingIntervals=c.stayingIntervals.filter((function(t){return t[0]!==l[0]&&t[1]!==l[1]}))),c.startExtent=s.region?l["s"===s.region?1:0]:a,e.parent.inBrushDrag=!0,c.brushStartCallback()}(this,t)})).on("drag",(function(t){w(this,t)})).on("dragend",(function(t){!function(t,e){var r=e.brush,n=r.filter,a=r.svgBrush;a._dragging||(T(t,e),w(t,e),e.brush.svgBrush.wasDragged=!1),a._dragging=!1,i.event.sourceEvent.stopPropagation();var o=a.grabbingBar;if(a.grabbingBar=!1,a.grabLocation=void 0,e.parent.inBrushDrag=!1,y(),!a.wasDragged)return a.wasDragged=void 0,a.clickableOrdinalRange?r.filterSpecified&&e.multiselect?a.extent.push(a.clickableOrdinalRange):(a.extent=[a.clickableOrdinalRange],r.filterSpecified=!0):o?(a.extent=a.stayingIntervals,0===a.extent.length&&M(r)):M(r),a.brushCallback(e),b(t.parentNode),void a.brushEndCallback(r.filterSpecified?n.getConsolidated():[]);var s=function(){n.set(n.getConsolidated())};if(e.ordinal){var l=e.unitTickvals;l[l.length-1]a.newExtent[0];a.extent=a.stayingIntervals.concat(c?[a.newExtent]:[]),a.extent.length||M(r),a.brushCallback(e),c?b(t.parentNode,s):(s(),b(t.parentNode))}else s();a.brushEndCallback(r.filterSpecified?n.getConsolidated():[])}(this,t)})))}function A(t,e){return t[0]-e[0]}function M(t){t.filterSpecified=!1,t.svgBrush.extent=[[-1/0,1/0]]}function S(t){for(var e,r=t.slice(),n=[],i=r.shift();i;){for(e=i.slice();(i=r.shift())&&i[0]<=e[1];)e[1]=Math.max(e[1],i[1]);n.push(e)}return 1===n.length&&n[0][0]>n[0][1]&&(n=[]),n}e.exports={makeBrush:function(t,e,r,n,i,a){var o,l=function(){var t,e,r=[];return{set:function(n){1===(r=n.map((function(t){return t.slice().sort(s)})).sort(A)).length&&r[0][0]===-1/0&&r[0][1]===1/0&&(r=[[0,-1]]),t=S(r),e=r.reduce((function(t,e){return[Math.min(t[0],e[0]),Math.max(t[1],e[1])]}),[1/0,-1/0])},get:function(){return r.slice()},getConsolidated:function(){return t},getBounds:function(){return e}}}();return l.set(r),{filter:l,filterSpecified:e,svgBrush:{extent:[],brushStartCallback:n,brushCallback:(o=i,function(t){var e=t.brush,r=function(t){return t.svgBrush.extent.map((function(t){return t.slice()}))}(e).slice();e.filter.set(r),o()}),brushEndCallback:a}}},ensureAxisBrush:function(t,e){var r=t.selectAll("."+n.cn.axisBrush).data(o,a);r.enter().append("g").classed(n.cn.axisBrush,!0),function(t,e){var r=t.selectAll(".background").data(o);r.enter().append("rect").classed("background",!0).call(d).call(m).style("pointer-events","auto").attr("transform",l(0,n.verticalPadding)),r.call(k).attr("height",(function(t){return t.height-n.verticalPadding}));var i=t.selectAll(".highlight-shadow").data(o);i.enter().append("line").classed("highlight-shadow",!0).attr("x",-n.bar.width/2).attr("stroke-width",n.bar.width+n.bar.strokeWidth).attr("stroke",e).attr("opacity",n.bar.strokeOpacity).attr("stroke-linecap","butt"),i.attr("y1",(function(t){return t.height})).call(x);var a=t.selectAll(".highlight").data(o);a.enter().append("line").classed("highlight",!0).attr("x",-n.bar.width/2).attr("stroke-width",n.bar.width-n.bar.strokeWidth).attr("stroke",n.bar.fillColor).attr("opacity",n.bar.fillOpacity).attr("stroke-linecap","butt"),a.attr("y1",(function(t){return t.height})).call(x)}(r,e)},cleanRanges:function(t,e){if(Array.isArray(t[0])?(t=t.map((function(t){return t.sort(s)})),t=e.multiselect?S(t.sort(A)):[t[0]]):t=[t.sort(s)],e.tickvals){var r=e.tickvals.slice().sort(s);if(!(t=t.map((function(t){var e=[p(0,r,t[0],[]),p(1,r,t[1],[])];if(e[1]>e[0])return e})).filter((function(t){return t}))).length)return}return t.length>1?t:t[0]}}},{"../../lib":503,"../../lib/gup":500,"./constants":893,"@plotly/d3":58}],890:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),supplyDefaults:t("./defaults"),calc:t("./calc"),colorbar:{container:"line",min:"cmin",max:"cmax"},moduleType:"trace",name:"parcoords",basePlotModule:t("./base_plot"),categories:["gl","regl","noOpacity","noHover"],meta:{}}},{"./attributes":888,"./base_plot":891,"./calc":892,"./defaults":894}],891:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../plots/get_data").getModuleCalcData,a=t("./plot"),o=t("../../constants/xmlns_namespaces");r.name="parcoords",r.plot=function(t){var e=i(t.calcdata,"parcoords")[0];e.length&&a(t,e)},r.clean=function(t,e,r,n){var i=n._has&&n._has("parcoords"),a=e._has&&e._has("parcoords");i&&!a&&(n._paperdiv.selectAll(".parcoords").remove(),n._glimages.selectAll("*").remove())},r.toSVG=function(t){var e=t._fullLayout._glimages,r=n.select(t).selectAll(".svg-container");r.filter((function(t,e){return e===r.size()-1})).selectAll(".gl-canvas-context, .gl-canvas-focus").each((function(){var t=this.toDataURL("image/png");e.append("svg:image").attr({xmlns:o.svg,"xlink:href":t,preserveAspectRatio:"none",x:0,y:0,width:this.style.width,height:this.style.height})})),window.setTimeout((function(){n.selectAll("#filterBarPattern").attr("id","filterBarPattern")}),60)}},{"../../constants/xmlns_namespaces":480,"../../plots/get_data":593,"./plot":900,"@plotly/d3":58}],892:[function(t,e,r){"use strict";var n=t("../../lib").isArrayOrTypedArray,i=t("../../components/colorscale"),a=t("../../lib/gup").wrap;e.exports=function(t,e){var r,o;return i.hasColorscale(e,"line")&&n(e.line.color)?(r=e.line.color,o=i.extractOpts(e.line).colorscale,i.calc(t,e,{vals:r,containerStr:"line",cLetter:"c"})):(r=function(t){for(var e=new Array(t),r=0;rf&&(n.log("parcoords traces support up to "+f+" dimensions at the moment"),d.splice(f));var m=s(t,e,{name:"dimensions",layout:l,handleItemDefaults:p}),g=function(t,e,r,o,s){var l=s("line.color",r);if(i(t,"line")&&n.isArrayOrTypedArray(l)){if(l.length)return s("line.colorscale"),a(t,e,o,s,{prefix:"line.",cLetter:"c"}),l.length;e.line.color=r}return 1/0}(t,e,r,l,u);o(e,l,u),Array.isArray(m)&&m.length||(e.visible=!1),h(e,m,"values",g);var v={family:l.font.family,size:Math.round(l.font.size/1.2),color:l.font.color};n.coerceFont(u,"labelfont",v),n.coerceFont(u,"tickfont",v),n.coerceFont(u,"rangefont",v),u("labelangle"),u("labelside")}},{"../../components/colorscale/defaults":376,"../../components/colorscale/helpers":377,"../../lib":503,"../../plots/array_container_defaults":549,"../../plots/cartesian/axes":554,"../../plots/domain":584,"./attributes":888,"./axisbrush":889,"./constants":893,"./merge_length":898}],895:[function(t,e,r){"use strict";var n=t("../../lib").isTypedArray;r.convertTypedArray=function(t){return n(t)?Array.prototype.slice.call(t):t},r.isOrdinal=function(t){return!!t.tickvals},r.isVisible=function(t){return t.visible||!("visible"in t)}},{"../../lib":503}],896:[function(t,e,r){"use strict";var n=t("./base_index");n.plot=t("./plot"),e.exports=n},{"./base_index":890,"./plot":900}],897:[function(t,e,r){"use strict";var n=t("glslify"),i=n(["precision highp float;\n#define GLSLIFY 1\n\nvarying vec4 fragColor;\n\nattribute vec4 p01_04, p05_08, p09_12, p13_16,\n p17_20, p21_24, p25_28, p29_32,\n p33_36, p37_40, p41_44, p45_48,\n p49_52, p53_56, p57_60, colors;\n\nuniform mat4 dim0A, dim1A, dim0B, dim1B, dim0C, dim1C, dim0D, dim1D,\n loA, hiA, loB, hiB, loC, hiC, loD, hiD;\n\nuniform vec2 resolution, viewBoxPos, viewBoxSize;\nuniform float maskHeight;\nuniform float drwLayer; // 0: context, 1: focus, 2: pick\nuniform vec4 contextColor;\nuniform sampler2D maskTexture, palette;\n\nbool isPick = (drwLayer > 1.5);\nbool isContext = (drwLayer < 0.5);\n\nconst vec4 ZEROS = vec4(0.0, 0.0, 0.0, 0.0);\nconst vec4 UNITS = vec4(1.0, 1.0, 1.0, 1.0);\n\nfloat val(mat4 p, mat4 v) {\n return dot(matrixCompMult(p, v) * UNITS, UNITS);\n}\n\nfloat axisY(float ratio, mat4 A, mat4 B, mat4 C, mat4 D) {\n float y1 = val(A, dim0A) + val(B, dim0B) + val(C, dim0C) + val(D, dim0D);\n float y2 = val(A, dim1A) + val(B, dim1B) + val(C, dim1C) + val(D, dim1D);\n return y1 * (1.0 - ratio) + y2 * ratio;\n}\n\nint iMod(int a, int b) {\n return a - b * (a / b);\n}\n\nbool fOutside(float p, float lo, float hi) {\n return (lo < hi) && (lo > p || p > hi);\n}\n\nbool vOutside(vec4 p, vec4 lo, vec4 hi) {\n return (\n fOutside(p[0], lo[0], hi[0]) ||\n fOutside(p[1], lo[1], hi[1]) ||\n fOutside(p[2], lo[2], hi[2]) ||\n fOutside(p[3], lo[3], hi[3])\n );\n}\n\nbool mOutside(mat4 p, mat4 lo, mat4 hi) {\n return (\n vOutside(p[0], lo[0], hi[0]) ||\n vOutside(p[1], lo[1], hi[1]) ||\n vOutside(p[2], lo[2], hi[2]) ||\n vOutside(p[3], lo[3], hi[3])\n );\n}\n\nbool outsideBoundingBox(mat4 A, mat4 B, mat4 C, mat4 D) {\n return mOutside(A, loA, hiA) ||\n mOutside(B, loB, hiB) ||\n mOutside(C, loC, hiC) ||\n mOutside(D, loD, hiD);\n}\n\nbool outsideRasterMask(mat4 A, mat4 B, mat4 C, mat4 D) {\n mat4 pnts[4];\n pnts[0] = A;\n pnts[1] = B;\n pnts[2] = C;\n pnts[3] = D;\n\n for(int i = 0; i < 4; ++i) {\n for(int j = 0; j < 4; ++j) {\n for(int k = 0; k < 4; ++k) {\n if(0 == iMod(\n int(255.0 * texture2D(maskTexture,\n vec2(\n (float(i * 2 + j / 2) + 0.5) / 8.0,\n (pnts[i][j][k] * (maskHeight - 1.0) + 1.0) / maskHeight\n ))[3]\n ) / int(pow(2.0, float(iMod(j * 4 + k, 8)))),\n 2\n )) return true;\n }\n }\n }\n return false;\n}\n\nvec4 position(bool isContext, float v, mat4 A, mat4 B, mat4 C, mat4 D) {\n float x = 0.5 * sign(v) + 0.5;\n float y = axisY(x, A, B, C, D);\n float z = 1.0 - abs(v);\n\n z += isContext ? 0.0 : 2.0 * float(\n outsideBoundingBox(A, B, C, D) ||\n outsideRasterMask(A, B, C, D)\n );\n\n return vec4(\n 2.0 * (vec2(x, y) * viewBoxSize + viewBoxPos) / resolution - 1.0,\n z,\n 1.0\n );\n}\n\nvoid main() {\n mat4 A = mat4(p01_04, p05_08, p09_12, p13_16);\n mat4 B = mat4(p17_20, p21_24, p25_28, p29_32);\n mat4 C = mat4(p33_36, p37_40, p41_44, p45_48);\n mat4 D = mat4(p49_52, p53_56, p57_60, ZEROS);\n\n float v = colors[3];\n\n gl_Position = position(isContext, v, A, B, C, D);\n\n fragColor =\n isContext ? vec4(contextColor) :\n isPick ? vec4(colors.rgb, 1.0) : texture2D(palette, vec2(abs(v), 0.5));\n}\n"]),a=n(["precision highp float;\n#define GLSLIFY 1\n\nvarying vec4 fragColor;\n\nvoid main() {\n gl_FragColor = fragColor;\n}\n"]),o=t("./constants").maxDimensionCount,s=t("../../lib"),l=new Uint8Array(4),c=new Uint8Array(4),u={shape:[256,1],format:"rgba",type:"uint8",mag:"nearest",min:"nearest"};function f(t,e,r,n,i){var a=t._gl;a.enable(a.SCISSOR_TEST),a.scissor(e,r,n,i),t.clear({color:[0,0,0,0],depth:1})}function h(t,e,r,n,i,a){var o=a.key;r.drawCompleted||(!function(t){t.read({x:0,y:0,width:1,height:1,data:l})}(t),r.drawCompleted=!0),function s(l){var c=Math.min(n,i-l*n);0===l&&(window.cancelAnimationFrame(r.currentRafs[o]),delete r.currentRafs[o],f(t,a.scissorX,a.scissorY,a.scissorWidth,a.viewBoxSize[1])),r.clearOnly||(a.count=2*c,a.offset=2*l*n,e(a),l*n+c>>8*e)%256/255}function m(t,e,r){for(var n=new Array(8*e),i=0,a=0;au&&(u=t[i].dim1.canvasX,o=i);0===s&&f(T,0,0,r.canvasWidth,r.canvasHeight);var p=function(t){var e,r,n,i=[[],[]];for(n=0;n<64;n++){var a=!t&&no._length&&(S=S.slice(0,o._length));var L,C=o.tickvals;function P(t,e){return{val:t,text:L[e]}}function I(t,e){return t.val-e.val}if(Array.isArray(C)&&C.length){L=o.ticktext,Array.isArray(L)&&L.length?L.length>C.length?L=L.slice(0,C.length):C.length>L.length&&(C=C.slice(0,L.length)):L=C.map(a(o.tickformat));for(var O=1;O=r||l>=i)return;var c=t.lineLayer.readPixel(s,i-1-l),u=0!==c[3],f=u?c[2]+256*(c[1]+256*c[0]):null,h={x:s,y:l,clientX:e.clientX,clientY:e.clientY,dataIndex:t.model.key,curveNumber:f};f!==B&&(u?a.hover(h):a.unhover&&a.unhover(h),B=f)}})),F.style("opacity",(function(t){return t.pick?0:1})),h.style("background","rgba(255, 255, 255, 0)");var N=h.selectAll("."+y.cn.parcoords).data(R,d);N.exit().remove(),N.enter().append("g").classed(y.cn.parcoords,!0).style("shape-rendering","crispEdges").style("pointer-events","none"),N.attr("transform",(function(t){return c(t.model.translateX,t.model.translateY)}));var j=N.selectAll("."+y.cn.parcoordsControlView).data(m,d);j.enter().append("g").classed(y.cn.parcoordsControlView,!0),j.attr("transform",(function(t){return c(t.model.pad.l,t.model.pad.t)}));var U=j.selectAll("."+y.cn.yAxis).data((function(t){return t.dimensions}),d);U.enter().append("g").classed(y.cn.yAxis,!0),j.each((function(t){O(U,t,_)})),F.each((function(t){if(t.viewModel){!t.lineLayer||a?t.lineLayer=b(this,t):t.lineLayer.update(t),(t.key||0===t.key)&&(t.viewModel[t.key]=t.lineLayer);var e=!t.context||a;t.lineLayer.render(t.viewModel.panels,e)}})),U.attr("transform",(function(t){return c(t.xScale(t.xIndex),0)})),U.call(n.behavior.drag().origin((function(t){return t})).on("drag",(function(t){var e=t.parent;S.linePickActive(!1),t.x=Math.max(-y.overdrag,Math.min(t.model.width+y.overdrag,n.event.x)),t.canvasX=t.x*t.model.canvasPixelRatio,U.sort((function(t,e){return t.x-e.x})).each((function(e,r){e.xIndex=r,e.x=t===e?e.x:e.xScale(e.xIndex),e.canvasX=e.x*e.model.canvasPixelRatio})),O(U,e,_),U.filter((function(e){return 0!==Math.abs(t.xIndex-e.xIndex)})).attr("transform",(function(t){return c(t.xScale(t.xIndex),0)})),n.select(this).attr("transform",c(t.x,0)),U.each((function(r,n,i){i===t.parent.key&&(e.dimensions[n]=r)})),e.contextLayer&&e.contextLayer.render(e.panels,!1,!E(e)),e.focusLayer.render&&e.focusLayer.render(e.panels)})).on("dragend",(function(t){var e=t.parent;t.x=t.xScale(t.xIndex),t.canvasX=t.x*t.model.canvasPixelRatio,O(U,e,_),n.select(this).attr("transform",(function(t){return c(t.x,0)})),e.contextLayer&&e.contextLayer.render(e.panels,!1,!E(e)),e.focusLayer&&e.focusLayer.render(e.panels),e.pickLayer&&e.pickLayer.render(e.panels,!0),S.linePickActive(!0),a&&a.axesMoved&&a.axesMoved(e.key,e.dimensions.map((function(t){return t.crossfilterDimensionIndex})))}))),U.exit().remove();var V=U.selectAll("."+y.cn.axisOverlays).data(m,d);V.enter().append("g").classed(y.cn.axisOverlays,!0),V.selectAll("."+y.cn.axis).remove();var H=V.selectAll("."+y.cn.axis).data(m,d);H.enter().append("g").classed(y.cn.axis,!0),H.each((function(t){var e=t.model.height/t.model.tickDistance,r=t.domainScale,i=r.domain();n.select(this).call(n.svg.axis().orient("left").tickSize(4).outerTickSize(2).ticks(e,t.tickFormat).tickValues(t.ordinal?i:null).tickFormat((function(e){return v.isOrdinal(t)?e:z(t.model.dimensions[t.visibleIndex],e)})).scale(r)),f.font(H.selectAll("text"),t.model.tickFont)})),H.selectAll(".domain, .tick>line").attr("fill","none").attr("stroke","black").attr("stroke-opacity",.25).attr("stroke-width","1px"),H.selectAll("text").style("text-shadow",u.makeTextShadow(T)).style("cursor","default");var q=V.selectAll("."+y.cn.axisHeading).data(m,d);q.enter().append("g").classed(y.cn.axisHeading,!0);var G=q.selectAll("."+y.cn.axisTitle).data(m,d);G.enter().append("text").classed(y.cn.axisTitle,!0).attr("text-anchor","middle").style("cursor","ew-resize").style("pointer-events","auto"),G.text((function(t){return t.label})).each((function(e){var r=n.select(this);f.font(r,e.model.labelFont),u.convertToTspans(r,t)})).attr("transform",(function(t){var e=I(t.model.labelAngle,t.model.labelSide),r=y.axisTitleOffset;return(e.dir>0?"":c(0,2*r+t.model.height))+l(e.degrees)+c(-r*e.dx,-r*e.dy)})).attr("text-anchor",(function(t){var e=I(t.model.labelAngle,t.model.labelSide);return 2*Math.abs(e.dx)>Math.abs(e.dy)?e.dir*e.dx<0?"start":"end":"middle"}));var Y=V.selectAll("."+y.cn.axisExtent).data(m,d);Y.enter().append("g").classed(y.cn.axisExtent,!0);var W=Y.selectAll("."+y.cn.axisExtentTop).data(m,d);W.enter().append("g").classed(y.cn.axisExtentTop,!0),W.attr("transform",c(0,-y.axisExtentOffset));var X=W.selectAll("."+y.cn.axisExtentTopText).data(m,d);X.enter().append("text").classed(y.cn.axisExtentTopText,!0).call(P),X.text((function(t){return D(t,!0)})).each((function(t){f.font(n.select(this),t.model.rangeFont)}));var Z=Y.selectAll("."+y.cn.axisExtentBottom).data(m,d);Z.enter().append("g").classed(y.cn.axisExtentBottom,!0),Z.attr("transform",(function(t){return c(0,t.model.height+y.axisExtentOffset)}));var J=Z.selectAll("."+y.cn.axisExtentBottomText).data(m,d);J.enter().append("text").classed(y.cn.axisExtentBottomText,!0).attr("dy","0.75em").call(P),J.text((function(t){return D(t,!1)})).each((function(t){f.font(n.select(this),t.model.rangeFont)})),x.ensureAxisBrush(V,T)}},{"../../components/colorscale":378,"../../components/drawing":388,"../../lib":503,"../../lib/gup":500,"../../lib/svg_text_utils":529,"../../plots/cartesian/axes":554,"./axisbrush":889,"./constants":893,"./helpers":895,"./lines":897,"@plotly/d3":58,"color-rgba":91}],900:[function(t,e,r){"use strict";var n=t("./parcoords"),i=t("../../lib/prepare_regl"),a=t("./helpers").isVisible,o={};function s(t,e,r){var n=e.indexOf(r),i=t.indexOf(n);return-1===i&&(i+=e.length),i}(e.exports=function(t,e){var r=t._fullLayout;if(i(t,[],o)){var l={},c={},u={},f={},h=r._size;e.forEach((function(e,r){var n=e[0].trace;u[r]=n.index;var i=f[r]=n._fullInput.index;l[r]=t.data[i].dimensions,c[r]=t.data[i].dimensions.slice()}));n(t,e,{width:h.w,height:h.h,margin:{t:h.t,r:h.r,b:h.b,l:h.l}},{filterChanged:function(e,n,i){var a=c[e][n],o=i.map((function(t){return t.slice()})),s="dimensions["+n+"].constraintrange",l=r._tracePreGUI[t._fullData[u[e]]._fullInput.uid];if(void 0===l[s]){var h=a.constraintrange;l[s]=h||null}var p=t._fullData[u[e]].dimensions[n];o.length?(1===o.length&&(o=o[0]),a.constraintrange=o,p.constraintrange=o.slice(),o=[o]):(delete a.constraintrange,delete p.constraintrange,o=null);var d={};d[s]=o,t.emit("plotly_restyle",[d,[f[e]]])},hover:function(e){t.emit("plotly_hover",e)},unhover:function(e){t.emit("plotly_unhover",e)},axesMoved:function(e,r){var n=function(t,e){return function(r,n){return s(t,e,r)-s(t,e,n)}}(r,c[e].filter(a));l[e].sort(n),c[e].filter((function(t){return!a(t)})).sort((function(t){return c[e].indexOf(t)})).forEach((function(t){l[e].splice(l[e].indexOf(t),1),l[e].splice(c[e].indexOf(t),0,t)})),t.emit("plotly_restyle",[{dimensions:[l[e]]},[f[e]]])}})}}).reglPrecompiled=o},{"../../lib/prepare_regl":516,"./helpers":895,"./parcoords":899}],901:[function(t,e,r){"use strict";var n=t("../../plots/attributes"),i=t("../../plots/domain").attributes,a=t("../../plots/font_attributes"),o=t("../../components/color/attributes"),s=t("../../plots/template_attributes").hovertemplateAttrs,l=t("../../plots/template_attributes").texttemplateAttrs,c=t("../../lib/extend").extendFlat,u=a({editType:"plot",arrayOk:!0,colorEditType:"plot"});e.exports={labels:{valType:"data_array",editType:"calc"},label0:{valType:"number",dflt:0,editType:"calc"},dlabel:{valType:"number",dflt:1,editType:"calc"},values:{valType:"data_array",editType:"calc"},marker:{colors:{valType:"data_array",editType:"calc"},line:{color:{valType:"color",dflt:o.defaultLine,arrayOk:!0,editType:"style"},width:{valType:"number",min:0,dflt:0,arrayOk:!0,editType:"style"},editType:"calc"},editType:"calc"},text:{valType:"data_array",editType:"plot"},hovertext:{valType:"string",dflt:"",arrayOk:!0,editType:"style"},scalegroup:{valType:"string",dflt:"",editType:"calc"},textinfo:{valType:"flaglist",flags:["label","text","value","percent"],extras:["none"],editType:"calc"},hoverinfo:c({},n.hoverinfo,{flags:["label","text","value","percent","name"]}),hovertemplate:s({},{keys:["label","color","value","percent","text"]}),texttemplate:l({editType:"plot"},{keys:["label","color","value","percent","text"]}),textposition:{valType:"enumerated",values:["inside","outside","auto","none"],dflt:"auto",arrayOk:!0,editType:"plot"},textfont:c({},u,{}),insidetextorientation:{valType:"enumerated",values:["horizontal","radial","tangential","auto"],dflt:"auto",editType:"plot"},insidetextfont:c({},u,{}),outsidetextfont:c({},u,{}),automargin:{valType:"boolean",dflt:!1,editType:"plot"},title:{text:{valType:"string",dflt:"",editType:"plot"},font:c({},u,{}),position:{valType:"enumerated",values:["top left","top center","top right","middle center","bottom left","bottom center","bottom right"],editType:"plot"},editType:"plot"},domain:i({name:"pie",trace:!0,editType:"calc"}),hole:{valType:"number",min:0,max:1,dflt:0,editType:"calc"},sort:{valType:"boolean",dflt:!0,editType:"calc"},direction:{valType:"enumerated",values:["clockwise","counterclockwise"],dflt:"counterclockwise",editType:"calc"},rotation:{valType:"number",min:-360,max:360,dflt:0,editType:"calc"},pull:{valType:"number",min:0,max:1,dflt:0,arrayOk:!0,editType:"calc"},_deprecated:{title:{valType:"string",dflt:"",editType:"calc"},titlefont:c({},u,{}),titleposition:{valType:"enumerated",values:["top left","top center","top right","middle center","bottom left","bottom center","bottom right"],editType:"calc"}}}},{"../../components/color/attributes":365,"../../lib/extend":493,"../../plots/attributes":550,"../../plots/domain":584,"../../plots/font_attributes":585,"../../plots/template_attributes":633}],902:[function(t,e,r){"use strict";var n=t("../../plots/plots");r.name="pie",r.plot=function(t,e,i,a){n.plotBasePlot(r.name,t,e,i,a)},r.clean=function(t,e,i,a){n.cleanBasePlot(r.name,t,e,i,a)}},{"../../plots/plots":619}],903:[function(t,e,r){"use strict";var n=t("fast-isnumeric"),i=t("tinycolor2"),a=t("../../components/color"),o={};function s(t){return function(e,r){return!!e&&(!!(e=i(e)).isValid()&&(e=a.addOpacity(e,e.getAlpha()),t[r]||(t[r]=e),e))}}function l(t,e){var r,n=JSON.stringify(t),a=e[n];if(!a){for(a=t.slice(),r=0;r=0})),("funnelarea"===e.type?v:e.sort)&&a.sort((function(t,e){return e.v-t.v})),a[0]&&(a[0].vTotal=g),a},crossTraceCalc:function(t,e){var r=(e||{}).type;r||(r="pie");var n=t._fullLayout,i=t.calcdata,a=n[r+"colorway"],s=n["_"+r+"colormap"];n["extend"+r+"colors"]&&(a=l(a,o));for(var c=0,u=0;u0){s=!0;break}}s||(o=0)}return{hasLabels:r,hasValues:a,len:o}}e.exports={handleLabelsAndValues:l,supplyDefaults:function(t,e,r,n){function c(r,n){return i.coerce(t,e,a,r,n)}var u=l(c("labels"),c("values")),f=u.len;if(e._hasLabels=u.hasLabels,e._hasValues=u.hasValues,!e._hasLabels&&e._hasValues&&(c("label0"),c("dlabel")),f){e._length=f,c("marker.line.width")&&c("marker.line.color"),c("marker.colors"),c("scalegroup");var h,p=c("text"),d=c("texttemplate");if(d||(h=c("textinfo",Array.isArray(p)?"text+percent":"percent")),c("hovertext"),c("hovertemplate"),d||h&&"none"!==h){var m=c("textposition");s(t,e,n,c,m,{moduleHasSelected:!1,moduleHasUnselected:!1,moduleHasConstrain:!1,moduleHasCliponaxis:!1,moduleHasTextangle:!1,moduleHasInsideanchor:!1}),(Array.isArray(m)||"auto"===m||"outside"===m)&&c("automargin"),("inside"===m||"auto"===m||Array.isArray(m))&&c("insidetextorientation")}o(e,n,c);var g=c("hole");if(c("title.text")){var v=c("title.position",g?"middle center":"top center");g||"middle center"!==v||(e.title.position="top center"),i.coerceFont(c,"title.font",n.font)}c("sort"),c("direction"),c("rotation"),c("pull")}else e.visible=!1}}},{"../../lib":503,"../../plots/domain":584,"../bar/defaults":652,"./attributes":901,"fast-isnumeric":190}],905:[function(t,e,r){"use strict";var n=t("../../components/fx/helpers").appendArrayMultiPointValues;e.exports=function(t,e){var r={curveNumber:e.index,pointNumbers:t.pts,data:e._input,fullData:e,label:t.label,color:t.color,value:t.v,percent:t.percent,text:t.text,bbox:t.bbox,v:t.v};return 1===t.pts.length&&(r.pointNumber=r.i=t.pts[0]),n(r,e,t.pts),"funnelarea"===e.type&&(delete r.v,delete r.i),r}},{"../../components/fx/helpers":402}],906:[function(t,e,r){"use strict";var n=t("../../lib");function i(t){return-1!==t.indexOf("e")?t.replace(/[.]?0+e/,"e"):-1!==t.indexOf(".")?t.replace(/[.]?0+$/,""):t}r.formatPiePercent=function(t,e){var r=i((100*t).toPrecision(3));return n.numSeparate(r,e)+"%"},r.formatPieValue=function(t,e){var r=i(t.toPrecision(10));return n.numSeparate(r,e)},r.getFirstFilled=function(t,e){if(Array.isArray(t))for(var r=0;r"),name:f.hovertemplate||-1!==h.indexOf("name")?f.name:void 0,idealAlign:t.pxmid[0]<0?"left":"right",color:g.castOption(_.bgcolor,t.pts)||t.color,borderColor:g.castOption(_.bordercolor,t.pts),fontFamily:g.castOption(w.family,t.pts),fontSize:g.castOption(w.size,t.pts),fontColor:g.castOption(w.color,t.pts),nameLength:g.castOption(_.namelength,t.pts),textAlign:g.castOption(_.align,t.pts),hovertemplate:g.castOption(f.hovertemplate,t.pts),hovertemplateLabels:t,eventData:[v(t,f)]},{container:r._hoverlayer.node(),outerContainer:r._paper.node(),gd:e,inOut_bbox:T}),t.bbox=T[0],c._hasHoverLabel=!0}c._hasHoverEvent=!0,e.emit("plotly_hover",{points:[v(t,f)],event:n.event})}})),t.on("mouseout",(function(t){var r=e._fullLayout,i=e._fullData[c.index],o=n.select(this).datum();c._hasHoverEvent&&(t.originalEvent=n.event,e.emit("plotly_unhover",{points:[v(o,i)],event:n.event}),c._hasHoverEvent=!1),c._hasHoverLabel&&(a.loneUnhover(r._hoverlayer.node()),c._hasHoverLabel=!1)})),t.on("click",(function(t){var r=e._fullLayout,i=e._fullData[c.index];e._dragging||!1===r.hovermode||(e._hoverdata=[v(t,i)],a.click(e,n.event))}))}function b(t,e,r){var n=g.castOption(t.insidetextfont.color,e.pts);!n&&t._input.textfont&&(n=g.castOption(t._input.textfont.color,e.pts));var i=g.castOption(t.insidetextfont.family,e.pts)||g.castOption(t.textfont.family,e.pts)||r.family,a=g.castOption(t.insidetextfont.size,e.pts)||g.castOption(t.textfont.size,e.pts)||r.size;return{color:n||o.contrast(e.color),family:i,size:a}}function _(t,e){for(var r,n,i=0;ie&&e>n||r=-4;g-=2)v(Math.PI*g,"tan");for(g=4;g>=-4;g-=2)v(Math.PI*(g+1),"tan")}if(f||p){for(g=4;g>=-4;g-=2)v(Math.PI*(g+1.5),"rad");for(g=4;g>=-4;g-=2)v(Math.PI*(g+.5),"rad")}}if(s||d||f){var y=Math.sqrt(t.width*t.width+t.height*t.height);if((a={scale:i*n*2/y,rCenter:1-i,rotate:0}).textPosAngle=(e.startangle+e.stopangle)/2,a.scale>=1)return a;m.push(a)}(d||p)&&((a=T(t,n,o,l,c)).textPosAngle=(e.startangle+e.stopangle)/2,m.push(a)),(d||h)&&((a=k(t,n,o,l,c)).textPosAngle=(e.startangle+e.stopangle)/2,m.push(a));for(var x=0,b=0,_=0;_=1)break}return m[x]}function T(t,e,r,n,i){e=Math.max(0,e-2*m);var a=t.width/t.height,o=S(a,n,e,r);return{scale:2*o/t.height,rCenter:A(a,o/e),rotate:M(i)}}function k(t,e,r,n,i){e=Math.max(0,e-2*m);var a=t.height/t.width,o=S(a,n,e,r);return{scale:2*o/t.width,rCenter:A(a,o/e),rotate:M(i+Math.PI/2)}}function A(t,e){return Math.cos(e)-t*e}function M(t){return(180/Math.PI*t+720)%180-90}function S(t,e,r,n){var i=t+1/(2*Math.tan(e));return r*Math.min(1/(Math.sqrt(i*i+.5)+i),n/(Math.sqrt(t*t+n/2)+t))}function E(t,e){return t.v!==e.vTotal||e.trace.hole?Math.min(1/(1+1/Math.sin(t.halfangle)),t.ring/2):1}function L(t,e){var r=e.pxmid[0],n=e.pxmid[1],i=t.width/2,a=t.height/2;return r<0&&(i*=-1),n<0&&(a*=-1),{scale:1,rCenter:1,rotate:0,x:i+Math.abs(a)*(i>0?1:-1)/2,y:a/(1+r*r/(n*n)),outside:!0}}function C(t,e){var r,n,i,a=t.trace,o={x:t.cx,y:t.cy},s={tx:0,ty:0};s.ty+=a.title.font.size,i=I(a),-1!==a.title.position.indexOf("top")?(o.y-=(1+i)*t.r,s.ty-=t.titleBox.height):-1!==a.title.position.indexOf("bottom")&&(o.y+=(1+i)*t.r);var l,c,u=(l=t.r,c=t.trace.aspectratio,l/(void 0===c?1:c)),f=e.w*(a.domain.x[1]-a.domain.x[0])/2;return-1!==a.title.position.indexOf("left")?(f+=u,o.x-=(1+i)*u,s.tx+=t.titleBox.width/2):-1!==a.title.position.indexOf("center")?f*=2:-1!==a.title.position.indexOf("right")&&(f+=u,o.x+=(1+i)*u,s.tx-=t.titleBox.width/2),r=f/t.titleBox.width,n=P(t,e)/t.titleBox.height,{x:o.x,y:o.y,scale:Math.min(r,n),tx:s.tx,ty:s.ty}}function P(t,e){var r=t.trace,n=e.h*(r.domain.y[1]-r.domain.y[0]);return Math.min(t.titleBox.height,n/2)}function I(t){var e,r=t.pull;if(!r)return 0;if(Array.isArray(r))for(r=0,e=0;er&&(r=t.pull[e]);return r}function O(t,e){for(var r=[],n=0;n1?(c=r.r,u=c/i.aspectratio):(u=r.r,c=u*i.aspectratio),c*=(1+i.baseratio)/2,l=c*u}o=Math.min(o,l/r.vTotal)}for(n=0;n")}if(a){var x=l.castOption(i,e.i,"texttemplate");if(x){var b=function(t){return{label:t.label,value:t.v,valueLabel:g.formatPieValue(t.v,n.separators),percent:t.v/r.vTotal,percentLabel:g.formatPiePercent(t.v/r.vTotal,n.separators),color:t.color,text:t.text,customdata:l.castOption(i,t.i,"customdata")}}(e),_=g.getFirstFilled(i.text,e.pts);(y(_)||""===_)&&(b.text=_),e.text=l.texttemplateString(x,b,t._fullLayout._d3locale,b,i._meta||{})}else e.text=""}}function R(t,e){var r=t.rotate*Math.PI/180,n=Math.cos(r),i=Math.sin(r),a=(e.left+e.right)/2,o=(e.top+e.bottom)/2;t.textX=a*n-o*i,t.textY=a*i+o*n,t.noCenter=!0}e.exports={plot:function(t,e){var r=t._fullLayout,a=r._size;d("pie",r),_(e,t),O(e,a);var h=l.makeTraceGroups(r._pielayer,e,"trace").each((function(e){var h=n.select(this),d=e[0],m=d.trace;!function(t){var e,r,n,i=t[0],a=i.r,o=i.trace,s=g.getRotationAngle(o.rotation),l=2*Math.PI/i.vTotal,c="px0",u="px1";if("counterclockwise"===o.direction){for(e=0;ei.vTotal/2?1:0,r.halfangle=Math.PI*Math.min(r.v/i.vTotal,.5),r.ring=1-o.hole,r.rInscribed=E(r,i))}(e),h.attr("stroke-linejoin","round"),h.each((function(){var v=n.select(this).selectAll("g.slice").data(e);v.enter().append("g").classed("slice",!0),v.exit().remove();var y=[[[],[]],[[],[]]],_=!1;v.each((function(i,a){if(i.hidden)n.select(this).selectAll("path,g").remove();else{i.pointNumber=i.i,i.curveNumber=m.index,y[i.pxmid[1]<0?0:1][i.pxmid[0]<0?0:1].push(i);var o=d.cx,c=d.cy,u=n.select(this),h=u.selectAll("path.surface").data([i]);if(h.enter().append("path").classed("surface",!0).style({"pointer-events":"all"}),u.call(x,t,e),m.pull){var v=+g.castOption(m.pull,i.pts)||0;v>0&&(o+=v*i.pxmid[0],c+=v*i.pxmid[1])}i.cxFinal=o,i.cyFinal=c;var T=m.hole;if(i.v===d.vTotal){var k="M"+(o+i.px0[0])+","+(c+i.px0[1])+C(i.px0,i.pxmid,!0,1)+C(i.pxmid,i.px0,!0,1)+"Z";T?h.attr("d","M"+(o+T*i.px0[0])+","+(c+T*i.px0[1])+C(i.px0,i.pxmid,!1,T)+C(i.pxmid,i.px0,!1,T)+"Z"+k):h.attr("d",k)}else{var A=C(i.px0,i.px1,!0,1);if(T){var M=1-T;h.attr("d","M"+(o+T*i.px1[0])+","+(c+T*i.px1[1])+C(i.px1,i.px0,!1,T)+"l"+M*i.px0[0]+","+M*i.px0[1]+A+"Z")}else h.attr("d","M"+o+","+c+"l"+i.px0[0]+","+i.px0[1]+A+"Z")}D(t,i,d);var S=g.castOption(m.textposition,i.pts),E=u.selectAll("g.slicetext").data(i.text&&"none"!==S?[0]:[]);E.enter().append("g").classed("slicetext",!0),E.exit().remove(),E.each((function(){var u=l.ensureSingle(n.select(this),"text","",(function(t){t.attr("data-notex",1)})),h=l.ensureUniformFontSize(t,"outside"===S?function(t,e,r){var n=g.castOption(t.outsidetextfont.color,e.pts)||g.castOption(t.textfont.color,e.pts)||r.color,i=g.castOption(t.outsidetextfont.family,e.pts)||g.castOption(t.textfont.family,e.pts)||r.family,a=g.castOption(t.outsidetextfont.size,e.pts)||g.castOption(t.textfont.size,e.pts)||r.size;return{color:n,family:i,size:a}}(m,i,r.font):b(m,i,r.font));u.text(i.text).attr({class:"slicetext",transform:"","text-anchor":"middle"}).call(s.font,h).call(f.convertToTspans,t);var v,y=s.bBox(u.node());if("outside"===S)v=L(y,i);else if(v=w(y,i,d),"auto"===S&&v.scale<1){var x=l.ensureUniformFontSize(t,m.outsidetextfont);u.call(s.font,x),v=L(y=s.bBox(u.node()),i)}var T=v.textPosAngle,k=void 0===T?i.pxmid:z(d.r,T);if(v.targetX=o+k[0]*v.rCenter+(v.x||0),v.targetY=c+k[1]*v.rCenter+(v.y||0),R(v,y),v.outside){var A=v.targetY;i.yLabelMin=A-y.height/2,i.yLabelMid=A,i.yLabelMax=A+y.height/2,i.labelExtraX=0,i.labelExtraY=0,_=!0}v.fontSize=h.size,p(m.type,v,r),e[a].transform=v,u.attr("transform",l.getTextTransform(v))}))}function C(t,e,r,n){var a=n*(e[0]-t[0]),o=n*(e[1]-t[1]);return"a"+n*d.r+","+n*d.r+" 0 "+i.largeArc+(r?" 1 ":" 0 ")+a+","+o}}));var T=n.select(this).selectAll("g.titletext").data(m.title.text?[0]:[]);if(T.enter().append("g").classed("titletext",!0),T.exit().remove(),T.each((function(){var e,r=l.ensureSingle(n.select(this),"text","",(function(t){t.attr("data-notex",1)})),i=m.title.text;m._meta&&(i=l.templateString(i,m._meta)),r.text(i).attr({class:"titletext",transform:"","text-anchor":"middle"}).call(s.font,m.title.font).call(f.convertToTspans,t),e="middle center"===m.title.position?function(t){var e=Math.sqrt(t.titleBox.width*t.titleBox.width+t.titleBox.height*t.titleBox.height);return{x:t.cx,y:t.cy,scale:t.trace.hole*t.r*2/e,tx:0,ty:-t.titleBox.height/2+t.trace.title.font.size}}(d):C(d,a),r.attr("transform",u(e.x,e.y)+c(Math.min(1,e.scale))+u(e.tx,e.ty))})),_&&function(t,e){var r,n,i,a,o,s,l,c,u,f,h,p,d;function m(t,e){return t.pxmid[1]-e.pxmid[1]}function v(t,e){return e.pxmid[1]-t.pxmid[1]}function y(t,r){r||(r={});var i,c,u,h,p=r.labelExtraY+(n?r.yLabelMax:r.yLabelMin),d=n?t.yLabelMin:t.yLabelMax,m=n?t.yLabelMax:t.yLabelMin,v=t.cyFinal+o(t.px0[1],t.px1[1]),y=p-d;if(y*l>0&&(t.labelExtraY=y),Array.isArray(e.pull))for(c=0;c=(g.castOption(e.pull,u.pts)||0)||((t.pxmid[1]-u.pxmid[1])*l>0?(y=u.cyFinal+o(u.px0[1],u.px1[1])-d-t.labelExtraY)*l>0&&(t.labelExtraY+=y):(m+t.labelExtraY-v)*l>0&&(i=3*s*Math.abs(c-f.indexOf(t)),(h=u.cxFinal+a(u.px0[0],u.px1[0])+i-(t.cxFinal+t.pxmid[0])-t.labelExtraX)*s>0&&(t.labelExtraX+=h)))}for(n=0;n<2;n++)for(i=n?m:v,o=n?Math.max:Math.min,l=n?1:-1,r=0;r<2;r++){for(a=r?Math.max:Math.min,s=r?1:-1,(c=t[n][r]).sort(i),u=t[1-n][r],f=u.concat(c),p=[],h=0;hMath.abs(f)?s+="l"+f*t.pxmid[0]/t.pxmid[1]+","+f+"H"+(a+t.labelExtraX+c):s+="l"+t.labelExtraX+","+u+"v"+(f-u)+"h"+c}else s+="V"+(t.yLabelMid+t.labelExtraY)+"h"+c;l.ensureSingle(r,"path","textline").call(o.stroke,e.outsidetextfont.color).attr({"stroke-width":Math.min(2,e.outsidetextfont.size/8),d:s,fill:"none"})}else r.select("path.textline").remove()}))}(v,m),_&&m.automargin){var k=s.bBox(h.node()),A=m.domain,M=a.w*(A.x[1]-A.x[0]),S=a.h*(A.y[1]-A.y[0]),E=(.5*M-d.r)/a.w,P=(.5*S-d.r)/a.h;i.autoMargin(t,"pie."+m.uid+".automargin",{xl:A.x[0]-E,xr:A.x[1]+E,yb:A.y[0]-P,yt:A.y[1]+P,l:Math.max(d.cx-d.r-k.left,0),r:Math.max(k.right-(d.cx+d.r),0),b:Math.max(k.bottom-(d.cy+d.r),0),t:Math.max(d.cy-d.r-k.top,0),pad:5})}}))}));setTimeout((function(){h.selectAll("tspan").each((function(){var t=n.select(this);t.attr("dy")&&t.attr("dy",t.attr("dy"))}))}),0)},formatSliceLabel:D,transformInsideText:w,determineInsideTextFont:b,positionTitleOutside:C,prerenderTitles:_,layoutAreas:O,attachFxHandlers:x,computeTransform:R}},{"../../components/color":366,"../../components/drawing":388,"../../components/fx":406,"../../lib":503,"../../lib/svg_text_utils":529,"../../plots/plots":619,"../bar/constants":650,"../bar/uniform_text":664,"./event_data":905,"./helpers":906,"@plotly/d3":58}],911:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("./style_one"),a=t("../bar/uniform_text").resizeText;e.exports=function(t){var e=t._fullLayout._pielayer.selectAll(".trace");a(t,e,"pie"),e.each((function(t){var e=t[0].trace,r=n.select(this);r.style({opacity:e.opacity}),r.selectAll("path.surface").each((function(t){n.select(this).call(i,t,e)}))}))}},{"../bar/uniform_text":664,"./style_one":912,"@plotly/d3":58}],912:[function(t,e,r){"use strict";var n=t("../../components/color"),i=t("./helpers").castOption;e.exports=function(t,e,r){var a=r.marker.line,o=i(a.color,e.pts)||n.defaultLine,s=i(a.width,e.pts)||0;t.style("stroke-width",s).call(n.fill,e.color).call(n.stroke,o)}},{"../../components/color":366,"./helpers":906}],913:[function(t,e,r){"use strict";var n=t("../scatter/attributes");e.exports={x:n.x,y:n.y,xy:{valType:"data_array",editType:"calc"},indices:{valType:"data_array",editType:"calc"},xbounds:{valType:"data_array",editType:"calc"},ybounds:{valType:"data_array",editType:"calc"},text:n.text,marker:{color:{valType:"color",arrayOk:!1,editType:"calc"},opacity:{valType:"number",min:0,max:1,dflt:1,arrayOk:!1,editType:"calc"},blend:{valType:"boolean",dflt:null,editType:"calc"},sizemin:{valType:"number",min:.1,max:2,dflt:.5,editType:"calc"},sizemax:{valType:"number",min:.1,dflt:20,editType:"calc"},border:{color:{valType:"color",arrayOk:!1,editType:"calc"},arearatio:{valType:"number",min:0,max:1,dflt:0,editType:"calc"},editType:"calc"},editType:"calc"},transforms:void 0}},{"../scatter/attributes":927}],914:[function(t,e,r){"use strict";var n=t("../../../stackgl_modules").gl_pointcloud2d,i=t("../../lib/str2rgbarray"),a=t("../../plots/cartesian/autorange").findExtremes,o=t("../scatter/get_trace_color");function s(t,e){this.scene=t,this.uid=e,this.type="pointcloud",this.pickXData=[],this.pickYData=[],this.xData=[],this.yData=[],this.textLabels=[],this.color="rgb(0, 0, 0)",this.name="",this.hoverinfo="all",this.idToIndex=new Int32Array(0),this.bounds=[0,0,0,0],this.pointcloudOptions={positions:new Float32Array(0),idToIndex:this.idToIndex,sizemin:.5,sizemax:12,color:[0,0,0,1],areaRatio:1,borderColor:[0,0,0,1]},this.pointcloud=n(t.glplot,this.pointcloudOptions),this.pointcloud._trace=this}var l=s.prototype;l.handlePick=function(t){var e=this.idToIndex[t.pointId];return{trace:this,dataCoord:t.dataCoord,traceCoord:this.pickXYData?[this.pickXYData[2*e],this.pickXYData[2*e+1]]:[this.pickXData[e],this.pickYData[e]],textLabel:Array.isArray(this.textLabels)?this.textLabels[e]:this.textLabels,color:this.color,name:this.name,pointIndex:e,hoverinfo:this.hoverinfo}},l.update=function(t){this.index=t.index,this.textLabels=t.text,this.name=t.name,this.hoverinfo=t.hoverinfo,this.bounds=[1/0,1/0,-1/0,-1/0],this.updateFast(t),this.color=o(t,{})},l.updateFast=function(t){var e,r,n,o,s,l,c=this.xData=this.pickXData=t.x,u=this.yData=this.pickYData=t.y,f=this.pickXYData=t.xy,h=t.xbounds&&t.ybounds,p=t.indices,d=this.bounds;if(f){if(n=f,e=f.length>>>1,h)d[0]=t.xbounds[0],d[2]=t.xbounds[1],d[1]=t.ybounds[0],d[3]=t.ybounds[1];else for(l=0;ld[2]&&(d[2]=o),sd[3]&&(d[3]=s);if(p)r=p;else for(r=new Int32Array(e),l=0;ld[2]&&(d[2]=o),sd[3]&&(d[3]=s);this.idToIndex=r,this.pointcloudOptions.idToIndex=r,this.pointcloudOptions.positions=n;var m=i(t.marker.color),g=i(t.marker.border.color),v=t.opacity*t.marker.opacity;m[3]*=v,this.pointcloudOptions.color=m;var y=t.marker.blend;if(null===y){y=c.length<100||u.length<100}this.pointcloudOptions.blend=y,g[3]*=v,this.pointcloudOptions.borderColor=g;var x=t.marker.sizemin,b=Math.max(t.marker.sizemax,t.marker.sizemin);this.pointcloudOptions.sizeMin=x,this.pointcloudOptions.sizeMax=b,this.pointcloudOptions.areaRatio=t.marker.border.arearatio,this.pointcloud.update(this.pointcloudOptions);var _=this.scene.xaxis,w=this.scene.yaxis,T=b/2||.5;t._extremes[_._id]=a(_,[d[0],d[2]],{ppad:T}),t._extremes[w._id]=a(w,[d[1],d[3]],{ppad:T})},l.dispose=function(){this.pointcloud.dispose()},e.exports=function(t,e){var r=new s(t,e.uid);return r.update(e),r}},{"../../../stackgl_modules":1124,"../../lib/str2rgbarray":528,"../../plots/cartesian/autorange":553,"../scatter/get_trace_color":937}],915:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("./attributes");e.exports=function(t,e,r){function a(r,a){return n.coerce(t,e,i,r,a)}a("x"),a("y"),a("xbounds"),a("ybounds"),t.xy&&t.xy instanceof Float32Array&&(e.xy=t.xy),t.indices&&t.indices instanceof Int32Array&&(e.indices=t.indices),a("text"),a("marker.color",r),a("marker.opacity"),a("marker.blend"),a("marker.sizemin"),a("marker.sizemax"),a("marker.border.color",r),a("marker.border.arearatio"),e._length=null}},{"../../lib":503,"./attributes":913}],916:[function(t,e,r){"use strict";["*pointcloud* trace is deprecated!","Please consider switching to the *scattergl* trace type."].join(" ");e.exports={attributes:t("./attributes"),supplyDefaults:t("./defaults"),calc:t("../scatter3d/calc"),plot:t("./convert"),moduleType:"trace",name:"pointcloud",basePlotModule:t("../../plots/gl2d"),categories:["gl","gl2d","showLegend"],meta:{}}},{"../../plots/gl2d":596,"../scatter3d/calc":956,"./attributes":913,"./convert":914,"./defaults":915}],917:[function(t,e,r){"use strict";var n=t("../../plots/font_attributes"),i=t("../../plots/attributes"),a=t("../../components/color/attributes"),o=t("../../components/fx/attributes"),s=t("../../plots/domain").attributes,l=t("../../plots/template_attributes").hovertemplateAttrs,c=t("../../components/colorscale/attributes"),u=t("../../plot_api/plot_template").templatedArray,f=t("../../plots/cartesian/axis_format_attributes").descriptionOnlyNumbers,h=t("../../lib/extend").extendFlat,p=t("../../plot_api/edit_types").overrideAll;(e.exports=p({hoverinfo:h({},i.hoverinfo,{flags:[],arrayOk:!1}),hoverlabel:o.hoverlabel,domain:s({name:"sankey",trace:!0}),orientation:{valType:"enumerated",values:["v","h"],dflt:"h"},valueformat:{valType:"string",dflt:".3s",description:f("value")},valuesuffix:{valType:"string",dflt:""},arrangement:{valType:"enumerated",values:["snap","perpendicular","freeform","fixed"],dflt:"snap"},textfont:n({}),customdata:void 0,node:{label:{valType:"data_array",dflt:[]},groups:{valType:"info_array",impliedEdits:{x:[],y:[]},dimensions:2,freeLength:!0,dflt:[],items:{valType:"number",editType:"calc"}},x:{valType:"data_array",dflt:[]},y:{valType:"data_array",dflt:[]},color:{valType:"color",arrayOk:!0},customdata:{valType:"data_array",editType:"calc"},line:{color:{valType:"color",dflt:a.defaultLine,arrayOk:!0},width:{valType:"number",min:0,dflt:.5,arrayOk:!0}},pad:{valType:"number",arrayOk:!1,min:0,dflt:20},thickness:{valType:"number",arrayOk:!1,min:1,dflt:20},hoverinfo:{valType:"enumerated",values:["all","none","skip"],dflt:"all"},hoverlabel:o.hoverlabel,hovertemplate:l({},{keys:["value","label"]})},link:{label:{valType:"data_array",dflt:[]},color:{valType:"color",arrayOk:!0},customdata:{valType:"data_array",editType:"calc"},line:{color:{valType:"color",dflt:a.defaultLine,arrayOk:!0},width:{valType:"number",min:0,dflt:0,arrayOk:!0}},source:{valType:"data_array",dflt:[]},target:{valType:"data_array",dflt:[]},value:{valType:"data_array",dflt:[]},hoverinfo:{valType:"enumerated",values:["all","none","skip"],dflt:"all"},hoverlabel:o.hoverlabel,hovertemplate:l({},{keys:["value","label"]}),colorscales:u("concentrationscales",{editType:"calc",label:{valType:"string",editType:"calc",dflt:""},cmax:{valType:"number",editType:"calc",dflt:1},cmin:{valType:"number",editType:"calc",dflt:0},colorscale:h(c().colorscale,{dflt:[[0,"white"],[1,"black"]]})})}},"calc","nested")).transforms=void 0},{"../../components/color/attributes":365,"../../components/colorscale/attributes":373,"../../components/fx/attributes":397,"../../lib/extend":493,"../../plot_api/edit_types":536,"../../plot_api/plot_template":543,"../../plots/attributes":550,"../../plots/cartesian/axis_format_attributes":557,"../../plots/domain":584,"../../plots/font_attributes":585,"../../plots/template_attributes":633}],918:[function(t,e,r){"use strict";var n=t("../../plot_api/edit_types").overrideAll,i=t("../../plots/get_data").getModuleCalcData,a=t("./plot"),o=t("../../components/fx/layout_attributes"),s=t("../../lib/setcursor"),l=t("../../components/dragelement"),c=t("../../plots/cartesian/select").prepSelect,u=t("../../lib"),f=t("../../registry");function h(t,e){var r=t._fullData[e],n=t._fullLayout,i=n.dragmode,a="pan"===n.dragmode?"move":"crosshair",o=r._bgRect;if("pan"!==i&&"zoom"!==i){s(o,a);var h={_id:"x",c2p:u.identity,_offset:r._sankey.translateX,_length:r._sankey.width},p={_id:"y",c2p:u.identity,_offset:r._sankey.translateY,_length:r._sankey.height},d={gd:t,element:o.node(),plotinfo:{id:e,xaxis:h,yaxis:p,fillRangeItems:u.noop},subplot:e,xaxes:[h],yaxes:[p],doneFnCompleted:function(r){var n,i=t._fullData[e],a=i.node.groups.slice(),o=[];function s(t){for(var e=i._sankey.graph.nodes,r=0;ry&&(y=a.source[e]),a.target[e]>y&&(y=a.target[e]);var x,b=y+1;t.node._count=b;var _=t.node.groups,w={};for(e=0;e<_.length;e++){var T=_[e];for(x=0;x0&&s(E,b)&&s(L,b)&&(!w.hasOwnProperty(E)||!w.hasOwnProperty(L)||w[E]!==w[L])){w.hasOwnProperty(L)&&(L=w[L]),w.hasOwnProperty(E)&&(E=w[E]),L=+L,h[E=+E]=h[L]=!0;var C="";a.label&&a.label[e]&&(C=a.label[e]);var P=null;C&&p.hasOwnProperty(C)&&(P=p[C]),c.push({pointNumber:e,label:C,color:u?a.color[e]:a.color,customdata:f?a.customdata[e]:a.customdata,concentrationscale:P,source:E,target:L,value:+S}),M.source.push(E),M.target.push(L)}}var I=b+_.length,O=o(r.color),z=o(r.customdata),D=[];for(e=0;eb-1,childrenNodes:[],pointNumber:e,label:R,color:O?r.color[e]:r.color,customdata:z?r.customdata[e]:r.customdata})}var F=!1;return function(t,e,r){for(var a=i.init2dArray(t,0),o=0;o1}))}(I,M.source,M.target)&&(F=!0),{circular:F,links:c,nodes:D,groups:_,groupLookup:w}}e.exports=function(t,e){var r=c(e);return a({circular:r.circular,_nodes:r.nodes,_links:r.links,_groups:r.groups,_groupLookup:r.groupLookup})}},{"../../components/colorscale":378,"../../lib":503,"../../lib/gup":500,"strongly-connected-components":306}],920:[function(t,e,r){"use strict";e.exports={nodeTextOffsetHorizontal:4,nodeTextOffsetVertical:3,nodePadAcross:10,sankeyIterations:50,forceIterations:5,forceTicksPerFrame:10,duration:500,ease:"linear",cn:{sankey:"sankey",sankeyLinks:"sankey-links",sankeyLink:"sankey-link",sankeyNodeSet:"sankey-node-set",sankeyNode:"sankey-node",nodeRect:"node-rect",nodeLabel:"node-label"}}},{}],921:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("./attributes"),a=t("../../components/color"),o=t("tinycolor2"),s=t("../../plots/domain").defaults,l=t("../../components/fx/hoverlabel_defaults"),c=t("../../plot_api/plot_template"),u=t("../../plots/array_container_defaults");function f(t,e){function r(r,a){return n.coerce(t,e,i.link.colorscales,r,a)}r("label"),r("cmin"),r("cmax"),r("colorscale")}e.exports=function(t,e,r,h){function p(r,a){return n.coerce(t,e,i,r,a)}var d=n.extendDeep(h.hoverlabel,t.hoverlabel),m=t.node,g=c.newContainer(e,"node");function v(t,e){return n.coerce(m,g,i.node,t,e)}v("label"),v("groups"),v("x"),v("y"),v("pad"),v("thickness"),v("line.color"),v("line.width"),v("hoverinfo",t.hoverinfo),l(m,g,v,d),v("hovertemplate");var y=h.colorway;v("color",g.label.map((function(t,e){return a.addOpacity(function(t){return y[t%y.length]}(e),.8)}))),v("customdata");var x=t.link||{},b=c.newContainer(e,"link");function _(t,e){return n.coerce(x,b,i.link,t,e)}_("label"),_("source"),_("target"),_("value"),_("line.color"),_("line.width"),_("hoverinfo",t.hoverinfo),l(x,b,_,d),_("hovertemplate");var w,T=o(h.paper_bgcolor).getLuminance()<.333?"rgba(255, 255, 255, 0.6)":"rgba(0, 0, 0, 0.2)";_("color",n.repeat(T,b.value.length)),_("customdata"),u(x,b,{name:"colorscales",handleItemDefaults:f}),s(e,h,p),p("orientation"),p("valueformat"),p("valuesuffix"),g.x.length&&g.y.length&&(w="freeform"),p("arrangement",w),n.coerceFont(p,"textfont",n.extendFlat({},h.font)),e._length=null}},{"../../components/color":366,"../../components/fx/hoverlabel_defaults":404,"../../lib":503,"../../plot_api/plot_template":543,"../../plots/array_container_defaults":549,"../../plots/domain":584,"./attributes":917,tinycolor2:312}],922:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),supplyDefaults:t("./defaults"),calc:t("./calc"),plot:t("./plot"),moduleType:"trace",name:"sankey",basePlotModule:t("./base_plot"),selectPoints:t("./select.js"),categories:["noOpacity"],meta:{}}},{"./attributes":917,"./base_plot":918,"./calc":919,"./defaults":921,"./plot":923,"./select.js":925}],923:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../lib"),a=i.numberFormat,o=t("./render"),s=t("../../components/fx"),l=t("../../components/color"),c=t("./constants").cn,u=i._;function f(t){return""!==t}function h(t,e){return t.filter((function(t){return t.key===e.traceId}))}function p(t,e){n.select(t).select("path").style("fill-opacity",e),n.select(t).select("rect").style("fill-opacity",e)}function d(t){n.select(t).select("text.name").style("fill","black")}function m(t){return function(e){return-1!==t.node.sourceLinks.indexOf(e.link)||-1!==t.node.targetLinks.indexOf(e.link)}}function g(t){return function(e){return-1!==e.node.sourceLinks.indexOf(t.link)||-1!==e.node.targetLinks.indexOf(t.link)}}function v(t,e,r){e&&r&&h(r,e).selectAll("."+c.sankeyLink).filter(m(e)).call(x.bind(0,e,r,!1))}function y(t,e,r){e&&r&&h(r,e).selectAll("."+c.sankeyLink).filter(m(e)).call(b.bind(0,e,r,!1))}function x(t,e,r,n){var i=n.datum().link.label;n.style("fill-opacity",(function(t){if(!t.link.concentrationscale)return.4})),i&&h(e,t).selectAll("."+c.sankeyLink).filter((function(t){return t.link.label===i})).style("fill-opacity",(function(t){if(!t.link.concentrationscale)return.4})),r&&h(e,t).selectAll("."+c.sankeyNode).filter(g(t)).call(v)}function b(t,e,r,n){var i=n.datum().link.label;n.style("fill-opacity",(function(t){return t.tinyColorAlpha})),i&&h(e,t).selectAll("."+c.sankeyLink).filter((function(t){return t.link.label===i})).style("fill-opacity",(function(t){return t.tinyColorAlpha})),r&&h(e,t).selectAll(c.sankeyNode).filter(g(t)).call(y)}function _(t,e){var r=t.hoverlabel||{},n=i.nestedProperty(r,e).get();return!Array.isArray(n)&&n}e.exports=function(t,e){for(var r=t._fullLayout,i=r._paper,h=r._size,m=0;m"),color:_(o,"bgcolor")||l.addOpacity(m.color,1),borderColor:_(o,"bordercolor"),fontFamily:_(o,"font.family"),fontSize:_(o,"font.size"),fontColor:_(o,"font.color"),nameLength:_(o,"namelength"),textAlign:_(o,"align"),idealAlign:n.event.x"),color:_(o,"bgcolor")||i.tinyColorHue,borderColor:_(o,"bordercolor"),fontFamily:_(o,"font.family"),fontSize:_(o,"font.size"),fontColor:_(o,"font.color"),nameLength:_(o,"namelength"),textAlign:_(o,"align"),idealAlign:"left",hovertemplate:o.hovertemplate,hovertemplateLabels:y,eventData:[i.node]},{container:r._hoverlayer.node(),outerContainer:r._paper.node(),gd:t});p(w,.85),d(w)}}},unhover:function(e,i,a){!1!==t._fullLayout.hovermode&&(n.select(e).call(y,i,a),"skip"!==i.node.trace.node.hoverinfo&&(i.node.fullData=i.node.trace,t.emit("plotly_unhover",{event:n.event,points:[i.node]})),s.loneUnhover(r._hoverlayer.node()))},select:function(e,r,i){var a=r.node;a.originalEvent=n.event,t._hoverdata=[a],n.select(e).call(y,r,i),s.click(t,{target:!0})}}})}},{"../../components/color":366,"../../components/fx":406,"../../lib":503,"./constants":920,"./render":924,"@plotly/d3":58}],924:[function(t,e,r){"use strict";var n=t("d3-force"),i=t("d3-interpolate").interpolateNumber,a=t("@plotly/d3"),o=t("@plotly/d3-sankey"),s=t("@plotly/d3-sankey-circular"),l=t("./constants"),c=t("tinycolor2"),u=t("../../components/color"),f=t("../../components/drawing"),h=t("../../lib"),p=h.strTranslate,d=h.strRotate,m=t("../../lib/gup"),g=m.keyFun,v=m.repeat,y=m.unwrap,x=t("../../lib/svg_text_utils"),b=t("../../registry"),_=t("../../constants/alignment"),w=_.CAP_SHIFT,T=_.LINE_SPACING;function k(t,e,r){var n,i=y(e),a=i.trace,u=a.domain,f="h"===a.orientation,p=a.node.pad,d=a.node.thickness,m=t.width*(u.x[1]-u.x[0]),g=t.height*(u.y[1]-u.y[0]),v=i._nodes,x=i._links,b=i.circular;(n=b?s.sankeyCircular().circularLinkGap(0):o.sankey()).iterations(l.sankeyIterations).size(f?[m,g]:[g,m]).nodeWidth(d).nodePadding(p).nodeId((function(t){return t.pointNumber})).nodes(v).links(x);var _,w,T,k=n();for(var A in n.nodePadding()=i||(r=i-e.y0)>1e-6&&(e.y0+=r,e.y1+=r),i=e.y1+p}))}(function(t){var e,r,n=t.map((function(t,e){return{x0:t.x0,index:e}})).sort((function(t,e){return t.x0-e.x0})),i=[],a=-1,o=-1/0;for(_=0;_o+d&&(a+=1,e=s.x0),o=s.x0,i[a]||(i[a]=[]),i[a].push(s),r=e-s.x0,s.x0+=r,s.x1+=r}return i}(v=k.nodes));n.update(k)}return{circular:b,key:r,trace:a,guid:h.randstr(),horizontal:f,width:m,height:g,nodePad:a.node.pad,nodeLineColor:a.node.line.color,nodeLineWidth:a.node.line.width,linkLineColor:a.link.line.color,linkLineWidth:a.link.line.width,valueFormat:a.valueformat,valueSuffix:a.valuesuffix,textFont:a.textfont,translateX:u.x[0]*t.width+t.margin.l,translateY:t.height-u.y[1]*t.height+t.margin.t,dragParallel:f?g:m,dragPerpendicular:f?m:g,arrangement:a.arrangement,sankey:n,graph:k,forceLayouts:{},interactionState:{dragInProgress:!1,hovered:!1}}}function A(t,e,r){var n=c(e.color),i=e.source.label+"|"+e.target.label+"__"+r;return e.trace=t.trace,e.curveNumber=t.trace.index,{circular:t.circular,key:i,traceId:t.key,pointNumber:e.pointNumber,link:e,tinyColorHue:u.tinyRGB(n),tinyColorAlpha:n.getAlpha(),linkPath:M,linkLineColor:t.linkLineColor,linkLineWidth:t.linkLineWidth,valueFormat:t.valueFormat,valueSuffix:t.valueSuffix,sankey:t.sankey,parent:t,interactionState:t.interactionState,flow:e.flow}}function M(){return function(t){if(t.link.circular)return e=t.link,r=e.width/2,n=e.circularPathData,"top"===e.circularLinkType?"M "+n.targetX+" "+(n.targetY+r)+" L"+n.rightInnerExtent+" "+(n.targetY+r)+"A"+(n.rightLargeArcRadius+r)+" "+(n.rightSmallArcRadius+r)+" 0 0 1 "+(n.rightFullExtent-r)+" "+(n.targetY-n.rightSmallArcRadius)+"L"+(n.rightFullExtent-r)+" "+n.verticalRightInnerExtent+"A"+(n.rightLargeArcRadius+r)+" "+(n.rightLargeArcRadius+r)+" 0 0 1 "+n.rightInnerExtent+" "+(n.verticalFullExtent-r)+"L"+n.leftInnerExtent+" "+(n.verticalFullExtent-r)+"A"+(n.leftLargeArcRadius+r)+" "+(n.leftLargeArcRadius+r)+" 0 0 1 "+(n.leftFullExtent+r)+" "+n.verticalLeftInnerExtent+"L"+(n.leftFullExtent+r)+" "+(n.sourceY-n.leftSmallArcRadius)+"A"+(n.leftLargeArcRadius+r)+" "+(n.leftSmallArcRadius+r)+" 0 0 1 "+n.leftInnerExtent+" "+(n.sourceY+r)+"L"+n.sourceX+" "+(n.sourceY+r)+"L"+n.sourceX+" "+(n.sourceY-r)+"L"+n.leftInnerExtent+" "+(n.sourceY-r)+"A"+(n.leftLargeArcRadius-r)+" "+(n.leftSmallArcRadius-r)+" 0 0 0 "+(n.leftFullExtent-r)+" "+(n.sourceY-n.leftSmallArcRadius)+"L"+(n.leftFullExtent-r)+" "+n.verticalLeftInnerExtent+"A"+(n.leftLargeArcRadius-r)+" "+(n.leftLargeArcRadius-r)+" 0 0 0 "+n.leftInnerExtent+" "+(n.verticalFullExtent+r)+"L"+n.rightInnerExtent+" "+(n.verticalFullExtent+r)+"A"+(n.rightLargeArcRadius-r)+" "+(n.rightLargeArcRadius-r)+" 0 0 0 "+(n.rightFullExtent+r)+" "+n.verticalRightInnerExtent+"L"+(n.rightFullExtent+r)+" "+(n.targetY-n.rightSmallArcRadius)+"A"+(n.rightLargeArcRadius-r)+" "+(n.rightSmallArcRadius-r)+" 0 0 0 "+n.rightInnerExtent+" "+(n.targetY-r)+"L"+n.targetX+" "+(n.targetY-r)+"Z":"M "+n.targetX+" "+(n.targetY-r)+" L"+n.rightInnerExtent+" "+(n.targetY-r)+"A"+(n.rightLargeArcRadius+r)+" "+(n.rightSmallArcRadius+r)+" 0 0 0 "+(n.rightFullExtent-r)+" "+(n.targetY+n.rightSmallArcRadius)+"L"+(n.rightFullExtent-r)+" "+n.verticalRightInnerExtent+"A"+(n.rightLargeArcRadius+r)+" "+(n.rightLargeArcRadius+r)+" 0 0 0 "+n.rightInnerExtent+" "+(n.verticalFullExtent+r)+"L"+n.leftInnerExtent+" "+(n.verticalFullExtent+r)+"A"+(n.leftLargeArcRadius+r)+" "+(n.leftLargeArcRadius+r)+" 0 0 0 "+(n.leftFullExtent+r)+" "+n.verticalLeftInnerExtent+"L"+(n.leftFullExtent+r)+" "+(n.sourceY+n.leftSmallArcRadius)+"A"+(n.leftLargeArcRadius+r)+" "+(n.leftSmallArcRadius+r)+" 0 0 0 "+n.leftInnerExtent+" "+(n.sourceY-r)+"L"+n.sourceX+" "+(n.sourceY-r)+"L"+n.sourceX+" "+(n.sourceY+r)+"L"+n.leftInnerExtent+" "+(n.sourceY+r)+"A"+(n.leftLargeArcRadius-r)+" "+(n.leftSmallArcRadius-r)+" 0 0 1 "+(n.leftFullExtent-r)+" "+(n.sourceY+n.leftSmallArcRadius)+"L"+(n.leftFullExtent-r)+" "+n.verticalLeftInnerExtent+"A"+(n.leftLargeArcRadius-r)+" "+(n.leftLargeArcRadius-r)+" 0 0 1 "+n.leftInnerExtent+" "+(n.verticalFullExtent-r)+"L"+n.rightInnerExtent+" "+(n.verticalFullExtent-r)+"A"+(n.rightLargeArcRadius-r)+" "+(n.rightLargeArcRadius-r)+" 0 0 1 "+(n.rightFullExtent+r)+" "+n.verticalRightInnerExtent+"L"+(n.rightFullExtent+r)+" "+(n.targetY+n.rightSmallArcRadius)+"A"+(n.rightLargeArcRadius-r)+" "+(n.rightSmallArcRadius-r)+" 0 0 1 "+n.rightInnerExtent+" "+(n.targetY+r)+"L"+n.targetX+" "+(n.targetY+r)+"Z";var e,r,n,a=t.link.source.x1,o=t.link.target.x0,s=i(a,o),l=s(.5),c=s(.5),u=t.link.y0-t.link.width/2,f=t.link.y0+t.link.width/2,h=t.link.y1-t.link.width/2,p=t.link.y1+t.link.width/2;return"M"+a+","+u+"C"+l+","+u+" "+c+","+h+" "+o+","+h+"L"+o+","+p+"C"+c+","+p+" "+l+","+f+" "+a+","+f+"Z"}}function S(t,e){var r=c(e.color),n=l.nodePadAcross,i=t.nodePad/2;e.dx=e.x1-e.x0,e.dy=e.y1-e.y0;var a=e.dx,o=Math.max(.5,e.dy),s="node_"+e.pointNumber;return e.group&&(s=h.randstr()),e.trace=t.trace,e.curveNumber=t.trace.index,{index:e.pointNumber,key:s,partOfGroup:e.partOfGroup||!1,group:e.group,traceId:t.key,trace:t.trace,node:e,nodePad:t.nodePad,nodeLineColor:t.nodeLineColor,nodeLineWidth:t.nodeLineWidth,textFont:t.textFont,size:t.horizontal?t.height:t.width,visibleWidth:Math.ceil(a),visibleHeight:o,zoneX:-n,zoneY:-i,zoneWidth:a+2*n,zoneHeight:o+2*i,labelY:t.horizontal?e.dy/2+1:e.dx/2+1,left:1===e.originalLayer,sizeAcross:t.width,forceLayouts:t.forceLayouts,horizontal:t.horizontal,darkBackground:r.getBrightness()<=128,tinyColorHue:u.tinyRGB(r),tinyColorAlpha:r.getAlpha(),valueFormat:t.valueFormat,valueSuffix:t.valueSuffix,sankey:t.sankey,graph:t.graph,arrangement:t.arrangement,uniqueNodeLabelPathId:[t.guid,t.key,s].join("_"),interactionState:t.interactionState,figure:t}}function E(t){t.attr("transform",(function(t){return p(t.node.x0.toFixed(3),t.node.y0.toFixed(3))}))}function L(t){t.call(E)}function C(t,e){t.call(L),e.attr("d",M())}function P(t){t.attr("width",(function(t){return t.node.x1-t.node.x0})).attr("height",(function(t){return t.visibleHeight}))}function I(t){return t.link.width>1||t.linkLineWidth>0}function O(t){return p(t.translateX,t.translateY)+(t.horizontal?"matrix(1 0 0 1 0 0)":"matrix(0 1 1 0 0 0)")}function z(t,e,r){t.on(".basic",null).on("mouseover.basic",(function(t){t.interactionState.dragInProgress||t.partOfGroup||(r.hover(this,t,e),t.interactionState.hovered=[this,t])})).on("mousemove.basic",(function(t){t.interactionState.dragInProgress||t.partOfGroup||(r.follow(this,t),t.interactionState.hovered=[this,t])})).on("mouseout.basic",(function(t){t.interactionState.dragInProgress||t.partOfGroup||(r.unhover(this,t,e),t.interactionState.hovered=!1)})).on("click.basic",(function(t){t.interactionState.hovered&&(r.unhover(this,t,e),t.interactionState.hovered=!1),t.interactionState.dragInProgress||t.partOfGroup||r.select(this,t,e)}))}function D(t,e,r,i){var o=a.behavior.drag().origin((function(t){return{x:t.node.x0+t.visibleWidth/2,y:t.node.y0+t.visibleHeight/2}})).on("dragstart",(function(a){if("fixed"!==a.arrangement&&(h.ensureSingle(i._fullLayout._infolayer,"g","dragcover",(function(t){i._fullLayout._dragCover=t})),h.raiseToTop(this),a.interactionState.dragInProgress=a.node,F(a.node),a.interactionState.hovered&&(r.nodeEvents.unhover.apply(0,a.interactionState.hovered),a.interactionState.hovered=!1),"snap"===a.arrangement)){var o=a.traceId+"|"+a.key;a.forceLayouts[o]?a.forceLayouts[o].alpha(1):function(t,e,r,i){!function(t){for(var e=0;e0&&n.forceLayouts[e].alpha(0)}}(0,e,a,r)).stop()}(0,o,a),function(t,e,r,n,i){window.requestAnimationFrame((function a(){var o;for(o=0;o0)window.requestAnimationFrame(a);else{var s=r.node.originalX;r.node.x0=s-r.visibleWidth/2,r.node.x1=s+r.visibleWidth/2,R(r,i)}}))}(t,e,a,o,i)}})).on("drag",(function(r){if("fixed"!==r.arrangement){var n=a.event.x,i=a.event.y;"snap"===r.arrangement?(r.node.x0=n-r.visibleWidth/2,r.node.x1=n+r.visibleWidth/2,r.node.y0=i-r.visibleHeight/2,r.node.y1=i+r.visibleHeight/2):("freeform"===r.arrangement&&(r.node.x0=n-r.visibleWidth/2,r.node.x1=n+r.visibleWidth/2),i=Math.max(0,Math.min(r.size-r.visibleHeight/2,i)),r.node.y0=i-r.visibleHeight/2,r.node.y1=i+r.visibleHeight/2),F(r.node),"snap"!==r.arrangement&&(r.sankey.update(r.graph),C(t.filter(B(r)),e))}})).on("dragend",(function(t){if("fixed"!==t.arrangement){t.interactionState.dragInProgress=!1;for(var e=0;el&&C[v].gap;)v--;for(x=C[v].s,m=C.length-1;m>v;m--)C[m].s=x;for(;lM[u]&&u=0;i--){var a=t[i];if("scatter"===a.type&&a.xaxis===r.xaxis&&a.yaxis===r.yaxis){a.opacity=void 0;break}}}}}},{}],934:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("../../registry"),a=t("./attributes"),o=t("./constants"),s=t("./subtypes"),l=t("./xy_defaults"),c=t("./period_defaults"),u=t("./stack_defaults"),f=t("./marker_defaults"),h=t("./line_defaults"),p=t("./line_shape_defaults"),d=t("./text_defaults"),m=t("./fillcolor_defaults"),g=t("../../lib").coercePattern;e.exports=function(t,e,r,v){function y(r,i){return n.coerce(t,e,a,r,i)}var x=l(t,e,v,y);if(x||(e.visible=!1),e.visible){c(t,e,v,y),y("xhoverformat"),y("yhoverformat");var b=u(t,e,v,y),_=!b&&x=Math.min(e,r)&&d<=Math.max(e,r)?0:1/0}var n=Math.max(3,t.mrc||0),i=1-1/n,a=Math.abs(h.c2p(t.x)-d);return a=Math.min(e,r)&&m<=Math.max(e,r)?0:1/0}var n=Math.max(3,t.mrc||0),i=1-1/n,a=Math.abs(p.c2p(t.y)-m);return aW!=(N=z[I][1])>=W&&(R=z[I-1][0],F=z[I][0],N-B&&(D=R+(F-R)*(W-B)/(N-B),H=Math.min(H,D),q=Math.max(q,D)));H=Math.max(H,0),q=Math.min(q,h._length);var X=s.defaultLine;return s.opacity(f.fillcolor)?X=f.fillcolor:s.opacity((f.line||{}).color)&&(X=f.line.color),n.extendFlat(t,{distance:t.maxHoverDistance,x0:H,x1:q,y0:W,y1:W,color:X,hovertemplate:!1}),delete t.index,f.text&&!Array.isArray(f.text)?t.text=String(f.text):t.text=f.name,[t]}}}},{"../../components/color":366,"../../components/fx":406,"../../lib":503,"../../registry":638,"./get_trace_color":937}],939:[function(t,e,r){"use strict";var n=t("./subtypes");e.exports={hasLines:n.hasLines,hasMarkers:n.hasMarkers,hasText:n.hasText,isBubble:n.isBubble,attributes:t("./attributes"),supplyDefaults:t("./defaults"),crossTraceDefaults:t("./cross_trace_defaults"),calc:t("./calc").calc,crossTraceCalc:t("./cross_trace_calc"),arraysToCalcdata:t("./arrays_to_calcdata"),plot:t("./plot"),colorbar:t("./marker_colorbar"),formatLabels:t("./format_labels"),style:t("./style").style,styleOnSelect:t("./style").styleOnSelect,hoverPoints:t("./hover"),selectPoints:t("./select"),animatable:!0,moduleType:"trace",name:"scatter",basePlotModule:t("../../plots/cartesian"),categories:["cartesian","svg","symbols","errorBarsOK","showLegend","scatter-like","zoomScale"],meta:{}}},{"../../plots/cartesian":568,"./arrays_to_calcdata":926,"./attributes":927,"./calc":928,"./cross_trace_calc":932,"./cross_trace_defaults":933,"./defaults":934,"./format_labels":936,"./hover":938,"./marker_colorbar":945,"./plot":948,"./select":949,"./style":951,"./subtypes":952}],940:[function(t,e,r){"use strict";var n=t("../../lib").isArrayOrTypedArray,i=t("../../components/colorscale/helpers").hasColorscale,a=t("../../components/colorscale/defaults");e.exports=function(t,e,r,o,s,l){var c=(t.marker||{}).color;(s("line.color",r),i(t,"line"))?a(t,e,o,s,{prefix:"line.",cLetter:"c"}):s("line.color",!n(c)&&c||r);s("line.width"),(l||{}).noDash||s("line.dash")}},{"../../components/colorscale/defaults":376,"../../components/colorscale/helpers":377,"../../lib":503}],941:[function(t,e,r){"use strict";var n=t("../../constants/numerical"),i=n.BADNUM,a=n.LOG_CLIP,o=a+.5,s=a-.5,l=t("../../lib"),c=l.segmentsIntersect,u=l.constrain,f=t("./constants");e.exports=function(t,e){var r,n,a,h,p,d,m,g,v,y,x,b,_,w,T,k,A,M,S=e.xaxis,E=e.yaxis,L="log"===S.type,C="log"===E.type,P=S._length,I=E._length,O=e.connectGaps,z=e.baseTolerance,D=e.shape,R="linear"===D,F=e.fill&&"none"!==e.fill,B=[],N=f.minTolerance,j=t.length,U=new Array(j),V=0;function H(r){var n=t[r];if(!n)return!1;var a=e.linearized?S.l2p(n.x):S.c2p(n.x),l=e.linearized?E.l2p(n.y):E.c2p(n.y);if(a===i){if(L&&(a=S.c2p(n.x,!0)),a===i)return!1;C&&l===i&&(a*=Math.abs(S._m*I*(S._m>0?o:s)/(E._m*P*(E._m>0?o:s)))),a*=1e3}if(l===i){if(C&&(l=E.c2p(n.y,!0)),l===i)return!1;l*=1e3}return[a,l]}function q(t,e,r,n){var i=r-t,a=n-e,o=.5-t,s=.5-e,l=i*i+a*a,c=i*o+a*s;if(c>0&&crt||t[1]it)return[u(t[0],et,rt),u(t[1],nt,it)]}function st(t,e){return t[0]===e[0]&&(t[0]===et||t[0]===rt)||(t[1]===e[1]&&(t[1]===nt||t[1]===it)||void 0)}function lt(t,e,r){return function(n,i){var a=ot(n),o=ot(i),s=[];if(a&&o&&st(a,o))return s;a&&s.push(a),o&&s.push(o);var c=2*l.constrain((n[t]+i[t])/2,e,r)-((a||n)[t]+(o||i)[t]);c&&((a&&o?c>0==a[t]>o[t]?a:o:a||o)[t]+=c);return s}}function ct(t){var e=t[0],r=t[1],n=e===U[V-1][0],i=r===U[V-1][1];if(!n||!i)if(V>1){var a=e===U[V-2][0],o=r===U[V-2][1];n&&(e===et||e===rt)&&a?o?V--:U[V-1]=t:i&&(r===nt||r===it)&&o?a?V--:U[V-1]=t:U[V++]=t}else U[V++]=t}function ut(t){U[V-1][0]!==t[0]&&U[V-1][1]!==t[1]&&ct([Z,J]),ct(t),K=null,Z=J=0}function ft(t){if(A=t[0]/P,M=t[1]/I,W=t[0]rt?rt:0,X=t[1]it?it:0,W||X){if(V)if(K){var e=$(K,t);e.length>1&&(ut(e[0]),U[V++]=e[1])}else Q=$(U[V-1],t)[0],U[V++]=Q;else U[V++]=[W||t[0],X||t[1]];var r=U[V-1];W&&X&&(r[0]!==W||r[1]!==X)?(K&&(Z!==W&&J!==X?ct(Z&&J?(n=K,a=(i=t)[0]-n[0],o=(i[1]-n[1])/a,(n[1]*i[0]-i[1]*n[0])/a>0?[o>0?et:rt,it]:[o>0?rt:et,nt]):[Z||W,J||X]):Z&&J&&ct([Z,J])),ct([W,X])):Z-W&&J-X&&ct([W||Z,X||J]),K=t,Z=W,J=X}else K&&ut($(K,t)[0]),U[V++]=t;var n,i,a,o}for("linear"===D||"spline"===D?$=function(t,e){for(var r=[],n=0,i=0;i<4;i++){var a=at[i],o=c(t[0],t[1],e[0],e[1],a[0],a[1],a[2],a[3]);o&&(!n||Math.abs(o.x-r[0][0])>1||Math.abs(o.y-r[0][1])>1)&&(o=[o.x,o.y],n&&Y(o,t)G(d,ht))break;a=d,(_=v[0]*g[0]+v[1]*g[1])>x?(x=_,h=d,m=!1):_=t.length||!d)break;ft(d),n=d}}else ft(h)}K&&ct([Z||K[0],J||K[1]]),B.push(U.slice(0,V))}return B}},{"../../constants/numerical":479,"../../lib":503,"./constants":931}],942:[function(t,e,r){"use strict";e.exports=function(t,e,r){"spline"===r("line.shape")&&r("line.smoothing")}},{}],943:[function(t,e,r){"use strict";var n={tonextx:1,tonexty:1,tonext:1};e.exports=function(t,e,r){var i,a,o,s,l,c={},u=!1,f=-1,h=0,p=-1;for(a=0;a=0?l=p:(l=p=h,h++),l0?Math.max(r,a):0}}},{"fast-isnumeric":190}],945:[function(t,e,r){"use strict";e.exports={container:"marker",min:"cmin",max:"cmax"}},{}],946:[function(t,e,r){"use strict";var n=t("../../components/color"),i=t("../../components/colorscale/helpers").hasColorscale,a=t("../../components/colorscale/defaults"),o=t("./subtypes");e.exports=function(t,e,r,s,l,c){var u=o.isBubble(t),f=(t.line||{}).color;(c=c||{},f&&(r=f),l("marker.symbol"),l("marker.opacity",u?.7:1),l("marker.size"),l("marker.color",r),i(t,"marker")&&a(t,e,s,l,{prefix:"marker.",cLetter:"c"}),c.noSelect||(l("selected.marker.color"),l("unselected.marker.color"),l("selected.marker.size"),l("unselected.marker.size")),c.noLine||(l("marker.line.color",f&&!Array.isArray(f)&&e.marker.color!==f?f:u?n.background:n.defaultLine),i(t,"marker.line")&&a(t,e,s,l,{prefix:"marker.line.",cLetter:"c"}),l("marker.line.width",u?1:0)),u&&(l("marker.sizeref"),l("marker.sizemin"),l("marker.sizemode")),c.gradient)&&("none"!==l("marker.gradient.type")&&l("marker.gradient.color"))}},{"../../components/color":366,"../../components/colorscale/defaults":376,"../../components/colorscale/helpers":377,"./subtypes":952}],947:[function(t,e,r){"use strict";var n=t("../../lib").dateTick0,i=t("../../constants/numerical").ONEWEEK;function a(t,e){return n(e,t%i==0?1:0)}e.exports=function(t,e,r,n,i){if(i||(i={x:!0,y:!0}),i.x){var o=n("xperiod");o&&(n("xperiod0",a(o,e.xcalendar)),n("xperiodalignment"))}if(i.y){var s=n("yperiod");s&&(n("yperiod0",a(s,e.ycalendar)),n("yperiodalignment"))}}},{"../../constants/numerical":479,"../../lib":503}],948:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../registry"),a=t("../../lib"),o=a.ensureSingle,s=a.identity,l=t("../../components/drawing"),c=t("./subtypes"),u=t("./line_points"),f=t("./link_traces"),h=t("../../lib/polygon").tester;function p(t,e,r,f,p,d,m){var g;!function(t,e,r,i,o){var s=r.xaxis,l=r.yaxis,u=n.extent(a.simpleMap(s.range,s.r2c)),f=n.extent(a.simpleMap(l.range,l.r2c)),h=i[0].trace;if(!c.hasMarkers(h))return;var p=h.marker.maxdisplayed;if(0===p)return;var d=i.filter((function(t){return t.x>=u[0]&&t.x<=u[1]&&t.y>=f[0]&&t.y<=f[1]})),m=Math.ceil(d.length/p),g=0;o.forEach((function(t,r){var n=t[0].trace;c.hasMarkers(n)&&n.marker.maxdisplayed>0&&r0;function y(t){return v?t.transition():t}var x=r.xaxis,b=r.yaxis,_=f[0].trace,w=_.line,T=n.select(d),k=o(T,"g","errorbars"),A=o(T,"g","lines"),M=o(T,"g","points"),S=o(T,"g","text");if(i.getComponentMethod("errorbars","plot")(t,k,r,m),!0===_.visible){var E,L;y(T).style("opacity",_.opacity);var C=_.fill.charAt(_.fill.length-1);"x"!==C&&"y"!==C&&(C=""),f[0][r.isRangePlot?"nodeRangePlot3":"node3"]=T;var P,I,O="",z=[],D=_._prevtrace;D&&(O=D._prevRevpath||"",L=D._nextFill,z=D._polygons);var R,F,B,N,j,U,V,H="",q="",G=[],Y=a.noop;if(E=_._ownFill,c.hasLines(_)||"none"!==_.fill){for(L&&L.datum(f),-1!==["hv","vh","hvh","vhv"].indexOf(w.shape)?(R=l.steps(w.shape),F=l.steps(w.shape.split("").reverse().join(""))):R=F="spline"===w.shape?function(t){var e=t[t.length-1];return t.length>1&&t[0][0]===e[0]&&t[0][1]===e[1]?l.smoothclosed(t.slice(1),w.smoothing):l.smoothopen(t,w.smoothing)}:function(t){return"M"+t.join("L")},B=function(t){return F(t.reverse())},G=u(f,{xaxis:x,yaxis:b,connectGaps:_.connectgaps,baseTolerance:Math.max(w.width||1,3)/4,shape:w.shape,simplify:w.simplify,fill:_.fill}),V=_._polygons=new Array(G.length),g=0;g1){var r=n.select(this);if(r.datum(f),t)y(r.style("opacity",0).attr("d",P).call(l.lineGroupStyle)).style("opacity",1);else{var i=y(r);i.attr("d",P),l.singleLineStyle(f,i)}}}}}var W=A.selectAll(".js-line").data(G);y(W.exit()).style("opacity",0).remove(),W.each(Y(!1)),W.enter().append("path").classed("js-line",!0).style("vector-effect","non-scaling-stroke").call(l.lineGroupStyle).each(Y(!0)),l.setClipUrl(W,r.layerClipId,t),G.length?(E?(E.datum(f),N&&U&&(C?("y"===C?N[1]=U[1]=b.c2p(0,!0):"x"===C&&(N[0]=U[0]=x.c2p(0,!0)),y(E).attr("d","M"+U+"L"+N+"L"+H.substr(1)).call(l.singleFillStyle,t)):y(E).attr("d",H+"Z").call(l.singleFillStyle,t))):L&&("tonext"===_.fill.substr(0,6)&&H&&O?("tonext"===_.fill?y(L).attr("d",H+"Z"+O+"Z").call(l.singleFillStyle,t):y(L).attr("d",H+"L"+O.substr(1)+"Z").call(l.singleFillStyle,t),_._polygons=_._polygons.concat(z)):(Z(L),_._polygons=null)),_._prevRevpath=q,_._prevPolygons=V):(E?Z(E):L&&Z(L),_._polygons=_._prevRevpath=_._prevPolygons=null),M.datum(f),S.datum(f),function(e,i,a){var o,u=a[0].trace,f=c.hasMarkers(u),h=c.hasText(u),p=tt(u),d=et,m=et;if(f||h){var g=s,_=u.stackgroup,w=_&&"infer zero"===t._fullLayout._scatterStackOpts[x._id+b._id][_].stackgaps;u.marker.maxdisplayed||u._needsCull?g=w?K:J:_&&!w&&(g=Q),f&&(d=g),h&&(m=g)}var T,k=(o=e.selectAll("path.point").data(d,p)).enter().append("path").classed("point",!0);v&&k.call(l.pointStyle,u,t).call(l.translatePoints,x,b).style("opacity",0).transition().style("opacity",1),o.order(),f&&(T=l.makePointStyleFns(u)),o.each((function(e){var i=n.select(this),a=y(i);l.translatePoint(e,a,x,b)?(l.singlePointStyle(e,a,u,T,t),r.layerClipId&&l.hideOutsideRangePoint(e,a,x,b,u.xcalendar,u.ycalendar),u.customdata&&i.classed("plotly-customdata",null!==e.data&&void 0!==e.data)):a.remove()})),v?o.exit().transition().style("opacity",0).remove():o.exit().remove(),(o=i.selectAll("g").data(m,p)).enter().append("g").classed("textpoint",!0).append("text"),o.order(),o.each((function(t){var e=n.select(this),i=y(e.select("text"));l.translatePoint(t,i,x,b)?r.layerClipId&&l.hideOutsideRangePoint(t,e,x,b,u.xcalendar,u.ycalendar):e.remove()})),o.selectAll("text").call(l.textPointStyle,u,t).each((function(t){var e=x.c2p(t.x),r=b.c2p(t.y);n.select(this).selectAll("tspan.line").each((function(){y(n.select(this)).attr({x:e,y:r})}))})),o.exit().remove()}(M,S,f);var X=!1===_.cliponaxis?null:r.layerClipId;l.setClipUrl(M,X,t),l.setClipUrl(S,X,t)}function Z(t){y(t).attr("d","M0,0Z")}function J(t){return t.filter((function(t){return!t.gap&&t.vis}))}function K(t){return t.filter((function(t){return t.vis}))}function Q(t){return t.filter((function(t){return!t.gap}))}function $(t){return t.id}function tt(t){if(t.ids)return $}function et(){return!1}}e.exports=function(t,e,r,i,a,c){var u,h,d=!a,m=!!a&&a.duration>0,g=f(t,e,r);((u=i.selectAll("g.trace").data(g,(function(t){return t[0].trace.uid}))).enter().append("g").attr("class",(function(t){return"trace scatter trace"+t[0].trace.uid})).style("stroke-miterlimit",2),u.order(),function(t,e,r){e.each((function(e){var i=o(n.select(this),"g","fills");l.setClipUrl(i,r.layerClipId,t);var a=e[0].trace,c=[];a._ownfill&&c.push("_ownFill"),a._nexttrace&&c.push("_nextFill");var u=i.selectAll("g").data(c,s);u.enter().append("g"),u.exit().each((function(t){a[t]=null})).remove(),u.order().each((function(t){a[t]=o(n.select(this),"path","js-fill")}))}))}(t,u,e),m)?(c&&(h=c()),n.transition().duration(a.duration).ease(a.easing).each("end",(function(){h&&h()})).each("interrupt",(function(){h&&h()})).each((function(){i.selectAll("g.trace").each((function(r,n){p(t,n,e,r,g,this,a)}))}))):u.each((function(r,n){p(t,n,e,r,g,this,a)}));d&&u.exit().remove(),i.selectAll("path:not([d])").remove()}},{"../../components/drawing":388,"../../lib":503,"../../lib/polygon":515,"../../registry":638,"./line_points":941,"./link_traces":943,"./subtypes":952,"@plotly/d3":58}],949:[function(t,e,r){"use strict";var n=t("./subtypes");e.exports=function(t,e){var r,i,a,o,s=t.cd,l=t.xaxis,c=t.yaxis,u=[],f=s[0].trace;if(!n.hasMarkers(f)&&!n.hasText(f))return[];if(!1===e)for(r=0;r0){var h=i.c2l(u);i._lowerLogErrorBound||(i._lowerLogErrorBound=h),i._lowerErrorBound=Math.min(i._lowerLogErrorBound,h)}}else o[s]=[-l[0]*r,l[1]*r]}return o}e.exports=function(t,e,r){var n=[i(t.x,t.error_x,e[0],r.xaxis),i(t.y,t.error_y,e[1],r.yaxis),i(t.z,t.error_z,e[2],r.zaxis)],a=function(t){for(var e=0;e-1?-1:t.indexOf("right")>-1?1:0}function b(t){return null==t?0:t.indexOf("top")>-1?-1:t.indexOf("bottom")>-1?1:0}function _(t,e){return e(4*t)}function w(t){return p[t]}function T(t,e,r,n,i){var a=null;if(l.isArrayOrTypedArray(t)){a=[];for(var o=0;o=0){var m=function(t,e,r){var n,i=(r+1)%3,a=(r+2)%3,o=[],l=[];for(n=0;n=0&&f("surfacecolor",h||p);for(var d=["x","y","z"],m=0;m<3;++m){var g="projection."+d[m];f(g+".show")&&(f(g+".opacity"),f(g+".scale"))}var v=n.getComponentMethod("errorbars","supplyDefaults");v(t,e,h||p||r,{axis:"z"}),v(t,e,h||p||r,{axis:"y",inherit:"z"}),v(t,e,h||p||r,{axis:"x",inherit:"z"})}else e.visible=!1}},{"../../lib":503,"../../registry":638,"../scatter/line_defaults":940,"../scatter/marker_defaults":946,"../scatter/subtypes":952,"../scatter/text_defaults":953,"./attributes":955}],960:[function(t,e,r){"use strict";e.exports={plot:t("./convert"),attributes:t("./attributes"),markerSymbols:t("../../constants/gl3d_markers"),supplyDefaults:t("./defaults"),colorbar:[{container:"marker",min:"cmin",max:"cmax"},{container:"line",min:"cmin",max:"cmax"}],calc:t("./calc"),moduleType:"trace",name:"scatter3d",basePlotModule:t("../../plots/gl3d"),categories:["gl3d","symbols","showLegend","scatter-like"],meta:{}}},{"../../constants/gl3d_markers":477,"../../plots/gl3d":598,"./attributes":955,"./calc":956,"./convert":958,"./defaults":959}],961:[function(t,e,r){"use strict";var n=t("../scatter/attributes"),i=t("../../plots/attributes"),a=t("../../plots/template_attributes").hovertemplateAttrs,o=t("../../plots/template_attributes").texttemplateAttrs,s=t("../../components/colorscale/attributes"),l=t("../../lib/extend").extendFlat,c=n.marker,u=n.line,f=c.line;e.exports={carpet:{valType:"string",editType:"calc"},a:{valType:"data_array",editType:"calc"},b:{valType:"data_array",editType:"calc"},mode:l({},n.mode,{dflt:"markers"}),text:l({},n.text,{}),texttemplate:o({editType:"plot"},{keys:["a","b","text"]}),hovertext:l({},n.hovertext,{}),line:{color:u.color,width:u.width,dash:u.dash,shape:l({},u.shape,{values:["linear","spline"]}),smoothing:u.smoothing,editType:"calc"},connectgaps:n.connectgaps,fill:l({},n.fill,{values:["none","toself","tonext"],dflt:"none"}),fillcolor:n.fillcolor,marker:l({symbol:c.symbol,opacity:c.opacity,maxdisplayed:c.maxdisplayed,size:c.size,sizeref:c.sizeref,sizemin:c.sizemin,sizemode:c.sizemode,line:l({width:f.width,editType:"calc"},s("marker.line")),gradient:c.gradient,editType:"calc"},s("marker")),textfont:n.textfont,textposition:n.textposition,selected:n.selected,unselected:n.unselected,hoverinfo:l({},i.hoverinfo,{flags:["a","b","text","name"]}),hoveron:n.hoveron,hovertemplate:a()}},{"../../components/colorscale/attributes":373,"../../lib/extend":493,"../../plots/attributes":550,"../../plots/template_attributes":633,"../scatter/attributes":927}],962:[function(t,e,r){"use strict";var n=t("fast-isnumeric"),i=t("../scatter/colorscale_calc"),a=t("../scatter/arrays_to_calcdata"),o=t("../scatter/calc_selection"),s=t("../scatter/calc").calcMarkerSize,l=t("../carpet/lookup_carpetid");e.exports=function(t,e){var r=e._carpetTrace=l(t,e);if(r&&r.visible&&"legendonly"!==r.visible){var c;e.xaxis=r.xaxis,e.yaxis=r.yaxis;var u,f,h=e._length,p=new Array(h),d=!1;for(c=0;c")}return o}function y(t,e){var r;r=t.labelprefix&&t.labelprefix.length>0?t.labelprefix.replace(/ = $/,""):t._hovertitle,g.push(r+": "+e.toFixed(3)+t.labelsuffix)}}},{"../../lib":503,"../scatter/hover":938}],967:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),supplyDefaults:t("./defaults"),colorbar:t("../scatter/marker_colorbar"),formatLabels:t("./format_labels"),calc:t("./calc"),plot:t("./plot"),style:t("../scatter/style").style,styleOnSelect:t("../scatter/style").styleOnSelect,hoverPoints:t("./hover"),selectPoints:t("../scatter/select"),eventData:t("./event_data"),moduleType:"trace",name:"scattercarpet",basePlotModule:t("../../plots/cartesian"),categories:["svg","carpet","symbols","showLegend","carpetDependent","zoomScale"],meta:{}}},{"../../plots/cartesian":568,"../scatter/marker_colorbar":945,"../scatter/select":949,"../scatter/style":951,"./attributes":961,"./calc":962,"./defaults":963,"./event_data":964,"./format_labels":965,"./hover":966,"./plot":968}],968:[function(t,e,r){"use strict";var n=t("../scatter/plot"),i=t("../../plots/cartesian/axes"),a=t("../../components/drawing");e.exports=function(t,e,r,o){var s,l,c,u=r[0][0].carpet,f={xaxis:i.getFromId(t,u.xaxis||"x"),yaxis:i.getFromId(t,u.yaxis||"y"),plot:e.plot};for(n(t,f,r,o),s=0;s")}(c,m,t,l[0].t.labels),t.hovertemplate=c.hovertemplate,[t]}}},{"../../components/fx":406,"../../constants/numerical":479,"../../lib":503,"../scatter/get_trace_color":937,"./attributes":969}],975:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),supplyDefaults:t("./defaults"),colorbar:t("../scatter/marker_colorbar"),formatLabels:t("./format_labels"),calc:t("./calc"),calcGeoJSON:t("./plot").calcGeoJSON,plot:t("./plot").plot,style:t("./style"),styleOnSelect:t("../scatter/style").styleOnSelect,hoverPoints:t("./hover"),eventData:t("./event_data"),selectPoints:t("./select"),moduleType:"trace",name:"scattergeo",basePlotModule:t("../../plots/geo"),categories:["geo","symbols","showLegend","scatter-like"],meta:{}}},{"../../plots/geo":589,"../scatter/marker_colorbar":945,"../scatter/style":951,"./attributes":969,"./calc":970,"./defaults":971,"./event_data":972,"./format_labels":973,"./hover":974,"./plot":976,"./select":977,"./style":978}],976:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../lib"),a=t("../../lib/topojson_utils").getTopojsonFeatures,o=t("../../lib/geojson_utils"),s=t("../../lib/geo_location_utils"),l=t("../../plots/cartesian/autorange").findExtremes,c=t("../../constants/numerical").BADNUM,u=t("../scatter/calc").calcMarkerSize,f=t("../scatter/subtypes"),h=t("./style");e.exports={calcGeoJSON:function(t,e){var r,n,i=t[0].trace,o=e[i.geo],f=o._subplot,h=i._length;if(Array.isArray(i.locations)){var p=i.locationmode,d="geojson-id"===p?s.extractTraceFeature(t):a(i,f.topojson);for(r=0;r=g,w=2*b,T={},k=l.makeCalcdata(e,"x"),A=y.makeCalcdata(e,"y"),M=s(e,l,"x",k),S=s(e,y,"y",A),E=M.vals,L=S.vals;e._x=E,e._y=L,e.xperiodalignment&&(e._origX=k,e._xStarts=M.starts,e._xEnds=M.ends),e.yperiodalignment&&(e._origY=A,e._yStarts=S.starts,e._yEnds=S.ends);var C=new Array(w),P=new Array(b);for(r=0;r1&&i.extendFlat(s.line,p.linePositions(t,r,n));if(s.errorX||s.errorY){var l=p.errorBarPositions(t,r,n,a,o);s.errorX&&i.extendFlat(s.errorX,l.x),s.errorY&&i.extendFlat(s.errorY,l.y)}s.text&&(i.extendFlat(s.text,{positions:n},p.textPosition(t,r,s.text,s.marker)),i.extendFlat(s.textSel,{positions:n},p.textPosition(t,r,s.text,s.markerSel)),i.extendFlat(s.textUnsel,{positions:n},p.textPosition(t,r,s.text,s.markerUnsel)));return s}(t,0,e,C,E,L),z=d(t,x);return f(o,e),_?O.marker&&(I=O.marker.sizeAvg||Math.max(O.marker.size,3)):I=c(e,b),u(t,e,l,y,E,L,I),O.errorX&&v(e,l,O.errorX),O.errorY&&v(e,y,O.errorY),O.fill&&!z.fill2d&&(z.fill2d=!0),O.marker&&!z.scatter2d&&(z.scatter2d=!0),O.line&&!z.line2d&&(z.line2d=!0),!O.errorX&&!O.errorY||z.error2d||(z.error2d=!0),O.text&&!z.glText&&(z.glText=!0),O.marker&&(O.marker.snap=b),z.lineOptions.push(O.line),z.errorXOptions.push(O.errorX),z.errorYOptions.push(O.errorY),z.fillOptions.push(O.fill),z.markerOptions.push(O.marker),z.markerSelectedOptions.push(O.markerSel),z.markerUnselectedOptions.push(O.markerUnsel),z.textOptions.push(O.text),z.textSelectedOptions.push(O.textSel),z.textUnselectedOptions.push(O.textUnsel),z.selectBatch.push([]),z.unselectBatch.push([]),T._scene=z,T.index=z.count,T.x=E,T.y=L,T.positions=C,z.count++,[{x:!1,y:!1,t:T,trace:e}]}},{"../../constants/numerical":479,"../../lib":503,"../../plots/cartesian/align_period":551,"../../plots/cartesian/autorange":553,"../../plots/cartesian/axis_ids":558,"../scatter/calc":928,"../scatter/colorscale_calc":930,"./constants":982,"./convert":983,"./scene_update":991,"@plotly/point-cluster":59}],982:[function(t,e,r){"use strict";e.exports={TOO_MANY_POINTS:1e5,SYMBOL_SDF_SIZE:200,SYMBOL_SIZE:20,SYMBOL_STROKE:1,DOT_RE:/-dot/,OPEN_RE:/-open/,DASHES:{solid:[1],dot:[1,1],dash:[4,1],longdash:[8,1],dashdot:[4,1,1,1],longdashdot:[8,1,1,1]}}},{}],983:[function(t,e,r){"use strict";var n=t("fast-isnumeric"),i=t("svg-path-sdf"),a=t("color-normalize"),o=t("../../registry"),s=t("../../lib"),l=t("../../components/drawing"),c=t("../../plots/cartesian/axis_ids"),u=t("../../lib/gl_format_color").formatColor,f=t("../scatter/subtypes"),h=t("../scatter/make_bubble_size_func"),p=t("./helpers"),d=t("./constants"),m=t("../../constants/interactions").DESELECTDIM,g={start:1,left:1,end:-1,right:-1,middle:0,center:0,bottom:1,top:-1},v=t("../../components/fx/helpers").appendArrayPointValue;function y(t,e){var r,i=t._fullLayout,a=e._length,o=e.textfont,l=e.textposition,c=Array.isArray(l)?l:[l],u=o.color,f=o.size,h=o.family,p={},d=t._context.plotGlPixelRatio,m=e.texttemplate;if(m){p.text=[];var g=i._d3locale,y=Array.isArray(m),x=y?Math.min(m.length,a):a,b=y?function(t){return m[t]}:function(){return m};for(r=0;rd.TOO_MANY_POINTS||f.hasMarkers(e)?"rect":"round";if(c&&e.connectgaps){var h=n[0],p=n[1];for(i=0;i1?l[i]:l[0]:l,d=Array.isArray(c)?c.length>1?c[i]:c[0]:c,m=g[p],v=g[d],y=u?u/.8+1:0,x=-v*y-.5*v;o.offset[i]=[m*y/h,x/h]}}return o}}},{"../../components/drawing":388,"../../components/fx/helpers":402,"../../constants/interactions":478,"../../lib":503,"../../lib/gl_format_color":499,"../../plots/cartesian/axis_ids":558,"../../registry":638,"../scatter/make_bubble_size_func":944,"../scatter/subtypes":952,"./constants":982,"./helpers":987,"color-normalize":89,"fast-isnumeric":190,"svg-path-sdf":310}],984:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("../../registry"),a=t("./helpers"),o=t("./attributes"),s=t("../scatter/constants"),l=t("../scatter/subtypes"),c=t("../scatter/xy_defaults"),u=t("../scatter/period_defaults"),f=t("../scatter/marker_defaults"),h=t("../scatter/line_defaults"),p=t("../scatter/fillcolor_defaults"),d=t("../scatter/text_defaults");e.exports=function(t,e,r,m){function g(r,i){return n.coerce(t,e,o,r,i)}var v=!!t.marker&&a.isOpenSymbol(t.marker.symbol),y=l.isBubble(t),x=c(t,e,m,g);if(x){u(t,e,m,g),g("xhoverformat"),g("yhoverformat");var b=x100},r.isDotSymbol=function(t){return"string"==typeof t?n.DOT_RE.test(t):t>200}},{"./constants":982}],988:[function(t,e,r){"use strict";var n=t("../../registry"),i=t("../../lib"),a=t("../scatter/get_trace_color");function o(t,e,r,o){var s=t.xa,l=t.ya,c=t.distance,u=t.dxy,f=t.index,h={pointNumber:f,x:e[f],y:r[f]};h.tx=Array.isArray(o.text)?o.text[f]:o.text,h.htx=Array.isArray(o.hovertext)?o.hovertext[f]:o.hovertext,h.data=Array.isArray(o.customdata)?o.customdata[f]:o.customdata,h.tp=Array.isArray(o.textposition)?o.textposition[f]:o.textposition;var p=o.textfont;p&&(h.ts=i.isArrayOrTypedArray(p.size)?p.size[f]:p.size,h.tc=Array.isArray(p.color)?p.color[f]:p.color,h.tf=Array.isArray(p.family)?p.family[f]:p.family);var d=o.marker;d&&(h.ms=i.isArrayOrTypedArray(d.size)?d.size[f]:d.size,h.mo=i.isArrayOrTypedArray(d.opacity)?d.opacity[f]:d.opacity,h.mx=i.isArrayOrTypedArray(d.symbol)?d.symbol[f]:d.symbol,h.mc=i.isArrayOrTypedArray(d.color)?d.color[f]:d.color);var m=d&&d.line;m&&(h.mlc=Array.isArray(m.color)?m.color[f]:m.color,h.mlw=i.isArrayOrTypedArray(m.width)?m.width[f]:m.width);var g=d&&d.gradient;g&&"none"!==g.type&&(h.mgt=Array.isArray(g.type)?g.type[f]:g.type,h.mgc=Array.isArray(g.color)?g.color[f]:g.color);var v=s.c2p(h.x,!0),y=l.c2p(h.y,!0),x=h.mrc||1,b=o.hoverlabel;b&&(h.hbg=Array.isArray(b.bgcolor)?b.bgcolor[f]:b.bgcolor,h.hbc=Array.isArray(b.bordercolor)?b.bordercolor[f]:b.bordercolor,h.hts=i.isArrayOrTypedArray(b.font.size)?b.font.size[f]:b.font.size,h.htc=Array.isArray(b.font.color)?b.font.color[f]:b.font.color,h.htf=Array.isArray(b.font.family)?b.font.family[f]:b.font.family,h.hnl=i.isArrayOrTypedArray(b.namelength)?b.namelength[f]:b.namelength);var _=o.hoverinfo;_&&(h.hi=Array.isArray(_)?_[f]:_);var w=o.hovertemplate;w&&(h.ht=Array.isArray(w)?w[f]:w);var T={};T[t.index]=h;var k=o._origX,A=o._origY,M=i.extendFlat({},t,{color:a(o,h),x0:v-x,x1:v+x,xLabelVal:k?k[f]:h.x,y0:y-x,y1:y+x,yLabelVal:A?A[f]:h.y,cd:T,distance:c,spikeDistance:u,hovertemplate:h.ht});return h.htx?M.text=h.htx:h.tx?M.text=h.tx:o.text&&(M.text=o.text),i.fillText(h,o,M),n.getComponentMethod("errorbars","hoverInfo")(h,o,M),M}e.exports={hoverPoints:function(t,e,r,n){var i,a,s,l,c,u,f,h,p,d,m=t.cd,g=m[0].t,v=m[0].trace,y=t.xa,x=t.ya,b=g.x,_=g.y,w=y.c2p(e),T=x.c2p(r),k=t.distance;if(g.tree){var A=y.p2c(w-k),M=y.p2c(w+k),S=x.p2c(T-k),E=x.p2c(T+k);i="x"===n?g.tree.range(Math.min(A,M),Math.min(x._rl[0],x._rl[1]),Math.max(A,M),Math.max(x._rl[0],x._rl[1])):g.tree.range(Math.min(A,M),Math.min(S,E),Math.max(A,M),Math.max(S,E))}else i=g.ids;var L=k;if("x"===n){var C=!!v.xperiodalignment,P=!!v.yperiodalignment;for(u=0;u=Math.min(I,O)&&w<=Math.max(I,O)?0:1/0}if(f=Math.min(z,D)&&T<=Math.max(z,D)?0:1/0}d=Math.sqrt(f*f+h*h),s=i[u]}}}else for(u=i.length-1;u>-1;u--)l=b[a=i[u]],c=_[a],f=y.c2p(l)-w,h=x.c2p(c)-T,(p=Math.sqrt(f*f+h*h))y.glText.length){var T=_-y.glText.length;for(m=0;mr&&(isNaN(e[n])||isNaN(e[n+1]));)n-=2;t.positions=e.slice(r,n+2)}return t})),y.line2d.update(y.lineOptions)),y.error2d){var A=(y.errorXOptions||[]).concat(y.errorYOptions||[]);y.error2d.update(A)}y.scatter2d&&y.scatter2d.update(y.markerOptions),y.fillOrder=s.repeat(null,_),y.fill2d&&(y.fillOptions=y.fillOptions.map((function(t,e){var n=r[e];if(t&&n&&n[0]&&n[0].trace){var i,a,o=n[0],s=o.trace,l=o.t,c=y.lineOptions[e],u=[];s._ownfill&&u.push(e),s._nexttrace&&u.push(e+1),u.length&&(y.fillOrder[e]=u);var f,h,p=[],d=c&&c.positions||l.positions;if("tozeroy"===s.fill){for(f=0;ff&&isNaN(d[h+1]);)h-=2;0!==d[f+1]&&(p=[d[f],0]),p=p.concat(d.slice(f,h+2)),0!==d[h+1]&&(p=p.concat([d[h],0]))}else if("tozerox"===s.fill){for(f=0;ff&&isNaN(d[h]);)h-=2;0!==d[f]&&(p=[0,d[f+1]]),p=p.concat(d.slice(f,h+2)),0!==d[h]&&(p=p.concat([0,d[h+1]]))}else if("toself"===s.fill||"tonext"===s.fill){for(p=[],i=0,t.splitNull=!0,a=0;a-1;for(m=0;m<_;m++){var L=r[m][0],C=L.trace,P=L.t,I=P.index,O=C._length,z=P.x,D=P.y;if(C.selectedpoints||S||E){if(S||(S=!0),C.selectedpoints){var R=y.selectBatch[I]=s.selIndices2selPoints(C),F={};for(g=0;g")}function u(t){return t+"\xb0"}}e.exports={hoverPoints:function(t,e,r){var o=t.cd,c=o[0].trace,u=t.xa,f=t.ya,h=t.subplot,p=360*(e>=0?Math.floor((e+180)/360):Math.ceil((e-180)/360)),d=e-p;if(n.getClosest(o,(function(t){var e=t.lonlat;if(e[0]===s)return 1/0;var n=i.modHalf(e[0],360),a=e[1],o=h.project([n,a]),l=o.x-u.c2p([d,a]),c=o.y-f.c2p([n,r]),p=Math.max(3,t.mrc||0);return Math.max(Math.sqrt(l*l+c*c)-p,1-3/p)}),t),!1!==t.index){var m=o[t.index],g=m.lonlat,v=[i.modHalf(g[0],360)+p,g[1]],y=u.c2p(v),x=f.c2p(v),b=m.mrc||1;t.x0=y-b,t.x1=y+b,t.y0=x-b,t.y1=x+b;var _={};_[c.subplot]={_subplot:h};var w=c._module.formatLabels(m,c,_);return t.lonLabel=w.lonLabel,t.latLabel=w.latLabel,t.color=a(c,m),t.extraText=l(c,m,o[0].t.labels),t.hovertemplate=c.hovertemplate,[t]}},getExtraText:l}},{"../../components/fx":406,"../../constants/numerical":479,"../../lib":503,"../scatter/get_trace_color":937}],999:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),supplyDefaults:t("./defaults"),colorbar:t("../scatter/marker_colorbar"),formatLabels:t("./format_labels"),calc:t("../scattergeo/calc"),plot:t("./plot"),hoverPoints:t("./hover").hoverPoints,eventData:t("./event_data"),selectPoints:t("./select"),styleOnSelect:function(t,e){e&&e[0].trace._glTrace.update(e)},moduleType:"trace",name:"scattermapbox",basePlotModule:t("../../plots/mapbox"),categories:["mapbox","gl","symbols","showLegend","scatter-like"],meta:{}}},{"../../plots/mapbox":613,"../scatter/marker_colorbar":945,"../scattergeo/calc":970,"./attributes":993,"./defaults":995,"./event_data":996,"./format_labels":997,"./hover":998,"./plot":1e3,"./select":1001}],1e3:[function(t,e,r){"use strict";var n=t("./convert"),i=t("../../plots/mapbox/constants").traceLayerPrefix,a=["fill","line","circle","symbol"];function o(t,e){this.type="scattermapbox",this.subplot=t,this.uid=e,this.sourceIds={fill:"source-"+e+"-fill",line:"source-"+e+"-line",circle:"source-"+e+"-circle",symbol:"source-"+e+"-symbol"},this.layerIds={fill:i+e+"-fill",line:i+e+"-line",circle:i+e+"-circle",symbol:i+e+"-symbol"},this.below=null}var s=o.prototype;s.addSource=function(t,e){this.subplot.map.addSource(this.sourceIds[t],{type:"geojson",data:e.geojson})},s.setSourceData=function(t,e){this.subplot.map.getSource(this.sourceIds[t]).setData(e.geojson)},s.addLayer=function(t,e,r){this.subplot.addLayer({type:t,id:this.layerIds[t],source:this.sourceIds[t],layout:e.layout,paint:e.paint},r)},s.update=function(t){var e,r,i,o=this.subplot,s=o.map,l=n(o.gd,t),c=o.belowLookup["trace-"+this.uid];if(c!==this.below){for(e=a.length-1;e>=0;e--)r=a[e],s.removeLayer(this.layerIds[r]);for(e=0;e=0;e--){var r=a[e];t.removeLayer(this.layerIds[r]),t.removeSource(this.sourceIds[r])}},e.exports=function(t,e){for(var r=e[0].trace,i=new o(t,r.uid),s=n(t.gd,e),l=i.below=t.belowLookup["trace-"+r.uid],c=0;c")}}e.exports={hoverPoints:function(t,e,r,a){var o=n(t,e,r,a);if(o&&!1!==o[0].index){var s=o[0];if(void 0===s.index)return o;var l=t.subplot,c=s.cd[s.index],u=s.trace;if(l.isPtInside(c))return s.xLabelVal=void 0,s.yLabelVal=void 0,i(c,u,l,s),s.hovertemplate=u.hovertemplate,o}},makeHoverPointText:i}},{"../scatter/hover":938}],1007:[function(t,e,r){"use strict";e.exports={moduleType:"trace",name:"scatterpolar",basePlotModule:t("../../plots/polar"),categories:["polar","symbols","showLegend","scatter-like"],attributes:t("./attributes"),supplyDefaults:t("./defaults").supplyDefaults,colorbar:t("../scatter/marker_colorbar"),formatLabels:t("./format_labels"),calc:t("./calc"),plot:t("./plot"),style:t("../scatter/style").style,styleOnSelect:t("../scatter/style").styleOnSelect,hoverPoints:t("./hover").hoverPoints,selectPoints:t("../scatter/select"),meta:{}}},{"../../plots/polar":622,"../scatter/marker_colorbar":945,"../scatter/select":949,"../scatter/style":951,"./attributes":1002,"./calc":1003,"./defaults":1004,"./format_labels":1005,"./hover":1006,"./plot":1008}],1008:[function(t,e,r){"use strict";var n=t("../scatter/plot"),i=t("../../constants/numerical").BADNUM;e.exports=function(t,e,r){for(var a=e.layers.frontplot.select("g.scatterlayer"),o={xaxis:e.xaxis,yaxis:e.yaxis,plot:e.framework,layerClipId:e._hasClipOnAxisFalse?e.clipIds.forTraces:null},s=e.radialAxis,l=e.angularAxis,c=0;c=c&&(y.marker.cluster=d.tree),y.marker&&(y.markerSel.positions=y.markerUnsel.positions=y.marker.positions=_),y.line&&_.length>1&&l.extendFlat(y.line,s.linePositions(t,p,_)),y.text&&(l.extendFlat(y.text,{positions:_},s.textPosition(t,p,y.text,y.marker)),l.extendFlat(y.textSel,{positions:_},s.textPosition(t,p,y.text,y.markerSel)),l.extendFlat(y.textUnsel,{positions:_},s.textPosition(t,p,y.text,y.markerUnsel))),y.fill&&!h.fill2d&&(h.fill2d=!0),y.marker&&!h.scatter2d&&(h.scatter2d=!0),y.line&&!h.line2d&&(h.line2d=!0),y.text&&!h.glText&&(h.glText=!0),h.lineOptions.push(y.line),h.fillOptions.push(y.fill),h.markerOptions.push(y.marker),h.markerSelectedOptions.push(y.markerSel),h.markerUnselectedOptions.push(y.markerUnsel),h.textOptions.push(y.text),h.textSelectedOptions.push(y.textSel),h.textUnselectedOptions.push(y.textUnsel),h.selectBatch.push([]),h.unselectBatch.push([]),d.x=w,d.y=T,d.rawx=w,d.rawy=T,d.r=g,d.theta=v,d.positions=_,d._scene=h,d.index=h.count,h.count++}})),a(t,e,r)}},e.exports.reglPrecompiled={}},{"../../lib":503,"../scattergl/constants":982,"../scattergl/convert":983,"../scattergl/plot":990,"../scattergl/scene_update":991,"@plotly/point-cluster":59,"fast-isnumeric":190}],1017:[function(t,e,r){"use strict";var n=t("../../plots/template_attributes").hovertemplateAttrs,i=t("../../plots/template_attributes").texttemplateAttrs,a=t("../../lib/extend").extendFlat,o=t("../scatter/attributes"),s=t("../../plots/attributes"),l=o.line;e.exports={mode:o.mode,real:{valType:"data_array",editType:"calc+clearAxisTypes"},imag:{valType:"data_array",editType:"calc+clearAxisTypes"},text:o.text,texttemplate:i({editType:"plot"},{keys:["real","imag","text"]}),hovertext:o.hovertext,line:{color:l.color,width:l.width,dash:l.dash,shape:a({},l.shape,{values:["linear","spline"]}),smoothing:l.smoothing,editType:"calc"},connectgaps:o.connectgaps,marker:o.marker,cliponaxis:a({},o.cliponaxis,{dflt:!1}),textposition:o.textposition,textfont:o.textfont,fill:a({},o.fill,{values:["none","toself","tonext"],dflt:"none"}),fillcolor:o.fillcolor,hoverinfo:a({},s.hoverinfo,{flags:["real","imag","text","name"]}),hoveron:o.hoveron,hovertemplate:n(),selected:o.selected,unselected:o.unselected}},{"../../lib/extend":493,"../../plots/attributes":550,"../../plots/template_attributes":633,"../scatter/attributes":927}],1018:[function(t,e,r){"use strict";var n=t("fast-isnumeric"),i=t("../../constants/numerical").BADNUM,a=t("../scatter/colorscale_calc"),o=t("../scatter/arrays_to_calcdata"),s=t("../scatter/calc_selection"),l=t("../scatter/calc").calcMarkerSize;e.exports=function(t,e){for(var r=t._fullLayout,c=e.subplot,u=r[c].realaxis,f=r[c].imaginaryaxis,h=u.makeCalcdata(e,"real"),p=f.makeCalcdata(e,"imag"),d=e._length,m=new Array(d),g=0;g")}}e.exports={hoverPoints:function(t,e,r,a){var o=n(t,e,r,a);if(o&&!1!==o[0].index){var s=o[0];if(void 0===s.index)return o;var l=t.subplot,c=s.cd[s.index],u=s.trace;if(l.isPtInside(c))return s.xLabelVal=void 0,s.yLabelVal=void 0,i(c,u,l,s),s.hovertemplate=u.hovertemplate,o}},makeHoverPointText:i}},{"../scatter/hover":938}],1022:[function(t,e,r){"use strict";e.exports={moduleType:"trace",name:"scattersmith",basePlotModule:t("../../plots/smith"),categories:["smith","symbols","showLegend","scatter-like"],attributes:t("./attributes"),supplyDefaults:t("./defaults"),colorbar:t("../scatter/marker_colorbar"),formatLabels:t("./format_labels"),calc:t("./calc"),plot:t("./plot"),style:t("../scatter/style").style,styleOnSelect:t("../scatter/style").styleOnSelect,hoverPoints:t("./hover").hoverPoints,selectPoints:t("../scatter/select"),meta:{}}},{"../../plots/smith":629,"../scatter/marker_colorbar":945,"../scatter/select":949,"../scatter/style":951,"./attributes":1017,"./calc":1018,"./defaults":1019,"./format_labels":1020,"./hover":1021,"./plot":1023}],1023:[function(t,e,r){"use strict";var n=t("../scatter/plot"),i=t("../../constants/numerical").BADNUM,a=t("../../plots/smith/helpers").smith;e.exports=function(t,e,r){for(var o=e.layers.frontplot.select("g.scatterlayer"),s={xaxis:e.xaxis,yaxis:e.yaxis,plot:e.framework,layerClipId:e._hasClipOnAxisFalse?e.clipIds.forTraces:null},l=0;l"),o.hovertemplate=h.hovertemplate,a}function x(t,e){v.push(t._hovertitle+": "+e)}}},{"../scatter/hover":938}],1030:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),supplyDefaults:t("./defaults"),colorbar:t("../scatter/marker_colorbar"),formatLabels:t("./format_labels"),calc:t("./calc"),plot:t("./plot"),style:t("../scatter/style").style,styleOnSelect:t("../scatter/style").styleOnSelect,hoverPoints:t("./hover"),selectPoints:t("../scatter/select"),eventData:t("./event_data"),moduleType:"trace",name:"scatterternary",basePlotModule:t("../../plots/ternary"),categories:["ternary","symbols","showLegend","scatter-like"],meta:{}}},{"../../plots/ternary":634,"../scatter/marker_colorbar":945,"../scatter/select":949,"../scatter/style":951,"./attributes":1024,"./calc":1025,"./defaults":1026,"./event_data":1027,"./format_labels":1028,"./hover":1029,"./plot":1031}],1031:[function(t,e,r){"use strict";var n=t("../scatter/plot");e.exports=function(t,e,r){var i=e.plotContainer;i.select(".scatterlayer").selectAll("*").remove();var a={xaxis:e.xaxis,yaxis:e.yaxis,plot:i,layerClipId:e._hasClipOnAxisFalse?e.clipIdRelative:null},o=e.layers.frontplot.select("g.scatterlayer");n(t,a,r,o)}},{"../scatter/plot":948}],1032:[function(t,e,r){"use strict";var n=t("../scatter/attributes"),i=t("../../components/colorscale/attributes"),a=t("../../plots/cartesian/axis_format_attributes").axisHoverFormat,o=t("../../plots/template_attributes").hovertemplateAttrs,s=t("../scattergl/attributes"),l=t("../../plots/cartesian/constants").idRegex,c=t("../../plot_api/plot_template").templatedArray,u=t("../../lib/extend").extendFlat,f=n.marker,h=f.line,p=u(i("marker.line",{editTypeOverride:"calc"}),{width:u({},h.width,{editType:"calc"}),editType:"calc"}),d=u(i("marker"),{symbol:f.symbol,size:u({},f.size,{editType:"markerSize"}),sizeref:f.sizeref,sizemin:f.sizemin,sizemode:f.sizemode,opacity:f.opacity,colorbar:f.colorbar,line:p,editType:"calc"});function m(t){return{valType:"info_array",freeLength:!0,editType:"calc",items:{valType:"subplotid",regex:l[t],editType:"plot"}}}d.color.editType=d.cmin.editType=d.cmax.editType="style",e.exports={dimensions:c("dimension",{visible:{valType:"boolean",dflt:!0,editType:"calc"},label:{valType:"string",editType:"calc"},values:{valType:"data_array",editType:"calc+clearAxisTypes"},axis:{type:{valType:"enumerated",values:["linear","log","date","category"],editType:"calc+clearAxisTypes"},matches:{valType:"boolean",dflt:!1,editType:"calc"},editType:"calc+clearAxisTypes"},editType:"calc+clearAxisTypes"}),text:u({},s.text,{}),hovertext:u({},s.hovertext,{}),hovertemplate:o(),xhoverformat:a("x"),yhoverformat:a("y"),marker:d,xaxes:m("x"),yaxes:m("y"),diagonal:{visible:{valType:"boolean",dflt:!0,editType:"calc"},editType:"calc"},showupperhalf:{valType:"boolean",dflt:!0,editType:"calc"},showlowerhalf:{valType:"boolean",dflt:!0,editType:"calc"},selected:{marker:s.selected.marker,editType:"calc"},unselected:{marker:s.unselected.marker,editType:"calc"},opacity:s.opacity}},{"../../components/colorscale/attributes":373,"../../lib/extend":493,"../../plot_api/plot_template":543,"../../plots/cartesian/axis_format_attributes":557,"../../plots/cartesian/constants":561,"../../plots/template_attributes":633,"../scatter/attributes":927,"../scattergl/attributes":979}],1033:[function(t,e,r){"use strict";var n=t("../../registry"),i=t("../../components/grid");e.exports={moduleType:"trace",name:"splom",categories:["gl","regl","cartesian","symbols","showLegend","scatter-like"],attributes:t("./attributes"),supplyDefaults:t("./defaults"),colorbar:t("../scatter/marker_colorbar"),calc:t("./calc"),plot:t("./plot"),hoverPoints:t("./hover").hoverPoints,selectPoints:t("./select"),editStyle:t("./edit_style"),meta:{}},n.register(i)},{"../../components/grid":410,"../../registry":638,"../scatter/marker_colorbar":945,"./attributes":1032,"./calc":1035,"./defaults":1036,"./edit_style":1037,"./hover":1039,"./plot":1041,"./select":1043}],1034:[function(t,e,r){"use strict";var n=t("regl-line2d"),i=t("../../registry"),a=t("../../lib/prepare_regl"),o=t("../../plots/get_data").getModuleCalcData,s=t("../../plots/cartesian"),l=t("../../plots/cartesian/axis_ids").getFromId,c=t("../../plots/cartesian/axes").shouldShowZeroLine,u={};function f(t,e,r){for(var n=r.matrixOptions.data.length,i=e._visibleDims,a=r.viewOpts.ranges=new Array(n),o=0;oh?b.sizeAvg||Math.max(b.size,3):a(e,x),p=0;pa&&l||i-1,P=!0;if(o(x)||!!p.selectedpoints||C){var I=p._length;if(p.selectedpoints){m.selectBatch=p.selectedpoints;var O=p.selectedpoints,z={};for(l=0;l1&&(u=m[y-1],h=g[y-1],d=v[y-1]),e=0;eu?"-":"+")+"x")).replace("y",(f>h?"-":"+")+"y")).replace("z",(p>d?"-":"+")+"z");var L=function(){y=0,M=[],S=[],E=[]};(!y||y2?t.slice(1,e-1):2===e?[(t[0]+t[1])/2]:t}function p(t){var e=t.length;return 1===e?[.5,.5]:[t[1]-t[0],t[e-1]-t[e-2]]}function d(t,e){var r=t.fullSceneLayout,i=t.dataScale,u=e._len,f={};function d(t,e){var n=r[e],o=i[c[e]];return a.simpleMap(t,(function(t){return n.d2l(t)*o}))}if(f.vectors=l(d(e._u,"xaxis"),d(e._v,"yaxis"),d(e._w,"zaxis"),u),!u)return{positions:[],cells:[]};var m=d(e._Xs,"xaxis"),g=d(e._Ys,"yaxis"),v=d(e._Zs,"zaxis");if(f.meshgrid=[m,g,v],f.gridFill=e._gridFill,e._slen)f.startingPositions=l(d(e._startsX,"xaxis"),d(e._startsY,"yaxis"),d(e._startsZ,"zaxis"));else{for(var y=g[0],x=h(m),b=h(v),_=new Array(x.length*b.length),w=0,T=0;T=0};v?(r=Math.min(g.length,x.length),l=function(t){return A(g[t])&&M(t)},f=function(t){return String(g[t])}):(r=Math.min(y.length,x.length),l=function(t){return A(y[t])&&M(t)},f=function(t){return String(y[t])}),_&&(r=Math.min(r,b.length));for(var S=0;S1){for(var P=a.randstr(),I=0;I"),name:A||z("name")?y.name:void 0,color:k("hoverlabel.bgcolor")||x.color,borderColor:k("hoverlabel.bordercolor"),fontFamily:k("hoverlabel.font.family"),fontSize:k("hoverlabel.font.size"),fontColor:k("hoverlabel.font.color"),nameLength:k("hoverlabel.namelength"),textAlign:k("hoverlabel.align"),hovertemplate:A,hovertemplateLabels:P,eventData:l};g&&(F.x0=E-i.rInscribed*i.rpx1,F.x1=E+i.rInscribed*i.rpx1,F.idealAlign=i.pxmid[0]<0?"left":"right"),v&&(F.x=E,F.idealAlign=E<0?"left":"right");var B=[];o.loneHover(F,{container:a._hoverlayer.node(),outerContainer:a._paper.node(),gd:r,inOut_bbox:B}),l[0].bbox=B[0],d._hasHoverLabel=!0}if(v){var N=t.select("path.surface");h.styleOne(N,i,y,{hovered:!0})}d._hasHoverEvent=!0,r.emit("plotly_hover",{points:l||[f(i,y,h.eventDataKeys)],event:n.event})}})),t.on("mouseout",(function(e){var i=r._fullLayout,a=r._fullData[d.index],s=n.select(this).datum();if(d._hasHoverEvent&&(e.originalEvent=n.event,r.emit("plotly_unhover",{points:[f(s,a,h.eventDataKeys)],event:n.event}),d._hasHoverEvent=!1),d._hasHoverLabel&&(o.loneUnhover(i._hoverlayer.node()),d._hasHoverLabel=!1),v){var l=t.select("path.surface");h.styleOne(l,s,a,{hovered:!1})}})),t.on("click",(function(t){var e=r._fullLayout,a=r._fullData[d.index],s=g&&(c.isHierarchyRoot(t)||c.isLeaf(t)),u=c.getPtId(t),p=c.isEntry(t)?c.findEntryWithChild(m,u):c.findEntryWithLevel(m,u),v=c.getPtId(p),y={points:[f(t,a,h.eventDataKeys)],event:n.event};s||(y.nextLevel=v);var x=l.triggerHandler(r,"plotly_"+d.type+"click",y);if(!1!==x&&e.hovermode&&(r._hoverdata=[f(t,a,h.eventDataKeys)],o.click(r,n.event)),!s&&!1!==x&&!r._dragging&&!r._transitioning){i.call("_storeDirectGUIEdit",a,e._tracePreGUI[a.uid],{level:a.level});var b={data:[{level:v}],traces:[d.index]},_={frame:{redraw:!1,duration:h.transitionTime},transition:{duration:h.transitionTime,easing:h.transitionEasing},mode:"immediate",fromcurrent:!0};o.loneUnhover(e._hoverlayer.node()),i.call("animate",r,b,_)}}))}},{"../../components/fx":406,"../../components/fx/helpers":402,"../../lib":503,"../../lib/events":492,"../../registry":638,"../pie/helpers":906,"./helpers":1055,"@plotly/d3":58}],1055:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("../../components/color"),a=t("../../lib/setcursor"),o=t("../pie/helpers");function s(t){return t.data.data.pid}r.findEntryWithLevel=function(t,e){var n;return e&&t.eachAfter((function(t){if(r.getPtId(t)===e)return n=t.copy()})),n||t},r.findEntryWithChild=function(t,e){var n;return t.eachAfter((function(t){for(var i=t.children||[],a=0;a0)},r.getMaxDepth=function(t){return t.maxdepth>=0?t.maxdepth:1/0},r.isHeader=function(t,e){return!(r.isLeaf(t)||t.depth===e._maxDepth-1)},r.getParent=function(t,e){return r.findEntryWithLevel(t,s(e))},r.listPath=function(t,e){var n=t.parent;if(!n)return[];var i=e?[n.data[e]]:[n];return r.listPath(n,e).concat(i)},r.getPath=function(t){return r.listPath(t,"label").join("/")+"/"},r.formatValue=o.formatPieValue,r.formatPercent=function(t,e){var r=n.formatPercent(t,0);return"0%"===r&&(r=o.formatPiePercent(t,e)),r}},{"../../components/color":366,"../../lib":503,"../../lib/setcursor":524,"../pie/helpers":906}],1056:[function(t,e,r){"use strict";e.exports={moduleType:"trace",name:"sunburst",basePlotModule:t("./base_plot"),categories:[],animatable:!0,attributes:t("./attributes"),layoutAttributes:t("./layout_attributes"),supplyDefaults:t("./defaults"),supplyLayoutDefaults:t("./layout_defaults"),calc:t("./calc").calc,crossTraceCalc:t("./calc").crossTraceCalc,plot:t("./plot").plot,style:t("./style").style,colorbar:t("../scatter/marker_colorbar"),meta:{}}},{"../scatter/marker_colorbar":945,"./attributes":1049,"./base_plot":1050,"./calc":1051,"./defaults":1053,"./layout_attributes":1057,"./layout_defaults":1058,"./plot":1059,"./style":1060}],1057:[function(t,e,r){"use strict";e.exports={sunburstcolorway:{valType:"colorlist",editType:"calc"},extendsunburstcolors:{valType:"boolean",dflt:!0,editType:"calc"}}},{}],1058:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("./layout_attributes");e.exports=function(t,e){function r(r,a){return n.coerce(t,e,i,r,a)}r("sunburstcolorway",e.colorway),r("extendsunburstcolors")}},{"../../lib":503,"./layout_attributes":1057}],1059:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("d3-hierarchy"),a=t("d3-interpolate").interpolate,o=t("../../components/drawing"),s=t("../../lib"),l=t("../../lib/svg_text_utils"),c=t("../bar/uniform_text"),u=c.recordMinTextSize,f=c.clearMinTextSize,h=t("../pie/plot"),p=t("../pie/helpers").getRotationAngle,d=h.computeTransform,m=h.transformInsideText,g=t("./style").styleOne,v=t("../bar/style").resizeText,y=t("./fx"),x=t("./constants"),b=t("./helpers");function _(t,e,c,f){var h=t._fullLayout,v=!h.uniformtext.mode&&b.hasTransition(f),_=n.select(c).selectAll("g.slice"),T=e[0],k=T.trace,A=T.hierarchy,M=b.findEntryWithLevel(A,k.level),S=b.getMaxDepth(k),E=h._size,L=k.domain,C=E.w*(L.x[1]-L.x[0]),P=E.h*(L.y[1]-L.y[0]),I=.5*Math.min(C,P),O=T.cx=E.l+E.w*(L.x[1]+L.x[0])/2,z=T.cy=E.t+E.h*(1-L.y[0])-P/2;if(!M)return _.remove();var D=null,R={};v&&_.each((function(t){R[b.getPtId(t)]={rpx0:t.rpx0,rpx1:t.rpx1,x0:t.x0,x1:t.x1,transform:t.transform},!D&&b.isEntry(t)&&(D=t)}));var F=function(t){return i.partition().size([2*Math.PI,t.height+1])(t)}(M).descendants(),B=M.height+1,N=0,j=S;T.hasMultipleRoots&&b.isHierarchyRoot(M)&&(F=F.slice(1),B-=1,N=1,j+=1),F=F.filter((function(t){return t.y1<=j}));var U=p(k.rotation);U&&F.forEach((function(t){t.x0+=U,t.x1+=U}));var V=Math.min(B,S),H=function(t){return(t-N)/V*I},q=function(t,e){return[t*Math.cos(e),-t*Math.sin(e)]},G=function(t){return s.pathAnnulus(t.rpx0,t.rpx1,t.x0,t.x1,O,z)},Y=function(t){return O+w(t)[0]*(t.transform.rCenter||0)+(t.transform.x||0)},W=function(t){return z+w(t)[1]*(t.transform.rCenter||0)+(t.transform.y||0)};(_=_.data(F,b.getPtId)).enter().append("g").classed("slice",!0),v?_.exit().transition().each((function(){var t=n.select(this);t.select("path.surface").transition().attrTween("d",(function(t){var e=function(t){var e,r=b.getPtId(t),n=R[r],i=R[b.getPtId(M)];if(i){var o=(t.x1>i.x1?2*Math.PI:0)+U;e=t.rpx1X?2*Math.PI:0)+U;e={x0:i,x1:i}}else e={rpx0:I,rpx1:I},s.extendFlat(e,K(t));else e={rpx0:0,rpx1:0};else e={x0:U,x1:U};return a(e,n)}(t);return function(t){return G(e(t))}})):f.attr("d",G),c.call(y,M,t,e,{eventDataKeys:x.eventDataKeys,transitionTime:x.CLICK_TRANSITION_TIME,transitionEasing:x.CLICK_TRANSITION_EASING}).call(b.setSliceCursor,t,{hideOnRoot:!0,hideOnLeaves:!0,isTransitioning:t._transitioning}),f.call(g,i,k);var p=s.ensureSingle(c,"g","slicetext"),_=s.ensureSingle(p,"text","",(function(t){t.attr("data-notex",1)})),w=s.ensureUniformFontSize(t,b.determineTextFont(k,i,h.font));_.text(r.formatSliceLabel(i,M,k,e,h)).classed("slicetext",!0).attr("text-anchor","middle").call(o.font,w).call(l.convertToTspans,t);var A=o.bBox(_.node());i.transform=m(A,i,T),i.transform.targetX=Y(i),i.transform.targetY=W(i);var S=function(t,e){var r=t.transform;return d(r,e),r.fontSize=w.size,u(k.type,r,h),s.getTextTransform(r)};v?_.transition().attrTween("transform",(function(t){var e=function(t){var e,r=R[b.getPtId(t)],n=t.transform;if(r)e=r;else if(e={rpx1:t.rpx1,transform:{textPosAngle:n.textPosAngle,scale:0,rotate:n.rotate,rCenter:n.rCenter,x:n.x,y:n.y}},D)if(t.parent)if(X){var i=t.x1>X?2*Math.PI:0;e.x0=e.x1=i}else s.extendFlat(e,K(t));else e.x0=e.x1=U;else e.x0=e.x1=U;var o=a(e.transform.textPosAngle,t.transform.textPosAngle),l=a(e.rpx1,t.rpx1),c=a(e.x0,t.x0),f=a(e.x1,t.x1),p=a(e.transform.scale,n.scale),d=a(e.transform.rotate,n.rotate),m=0===n.rCenter?3:0===e.transform.rCenter?1/3:1,g=a(e.transform.rCenter,n.rCenter);return function(t){var e=l(t),r=c(t),i=f(t),a=function(t){return g(Math.pow(t,m))}(t),s={pxmid:q(e,(r+i)/2),rpx1:e,transform:{textPosAngle:o(t),rCenter:a,x:n.x,y:n.y}};return u(k.type,n,h),{transform:{targetX:Y(s),targetY:W(s),scale:p(t),rotate:d(t),rCenter:a}}}}(t);return function(t){return S(e(t),A)}})):_.attr("transform",S(i,A))}))}function w(t){return e=t.rpx1,r=t.transform.textPosAngle,[e*Math.sin(r),-e*Math.cos(r)];var e,r}r.plot=function(t,e,r,i){var a,o,s=t._fullLayout,l=s._sunburstlayer,c=!r,u=!s.uniformtext.mode&&b.hasTransition(r);(f("sunburst",s),(a=l.selectAll("g.trace.sunburst").data(e,(function(t){return t[0].trace.uid}))).enter().append("g").classed("trace",!0).classed("sunburst",!0).attr("stroke-linejoin","round"),a.order(),u)?(i&&(o=i()),n.transition().duration(r.duration).ease(r.easing).each("end",(function(){o&&o()})).each("interrupt",(function(){o&&o()})).each((function(){l.selectAll("g.trace").each((function(e){_(t,e,this,r)}))}))):(a.each((function(e){_(t,e,this,r)})),s.uniformtext.mode&&v(t,s._sunburstlayer.selectAll(".trace"),"sunburst"));c&&a.exit().remove()},r.formatSliceLabel=function(t,e,r,n,i){var a=r.texttemplate,o=r.textinfo;if(!(a||o&&"none"!==o))return"";var l=i.separators,c=n[0],u=t.data.data,f=c.hierarchy,h=b.isHierarchyRoot(t),p=b.getParent(f,t),d=b.getValue(t);if(!a){var m,g=o.split("+"),v=function(t){return-1!==g.indexOf(t)},y=[];if(v("label")&&u.label&&y.push(u.label),u.hasOwnProperty("v")&&v("value")&&y.push(b.formatValue(u.v,l)),!h){v("current path")&&y.push(b.getPath(t.data));var x=0;v("percent parent")&&x++,v("percent entry")&&x++,v("percent root")&&x++;var _=x>1;if(x){var w,T=function(t){m=b.formatPercent(w,l),_&&(m+=" of "+t),y.push(m)};v("percent parent")&&!h&&(w=d/b.getValue(p),T("parent")),v("percent entry")&&(w=d/b.getValue(e),T("entry")),v("percent root")&&(w=d/b.getValue(f),T("root"))}}return v("text")&&(m=s.castOption(r,u.i,"text"),s.isValidTextValue(m)&&y.push(m)),y.join("
")}var k=s.castOption(r,u.i,"texttemplate");if(!k)return"";var A={};u.label&&(A.label=u.label),u.hasOwnProperty("v")&&(A.value=u.v,A.valueLabel=b.formatValue(u.v,l)),A.currentPath=b.getPath(t.data),h||(A.percentParent=d/b.getValue(p),A.percentParentLabel=b.formatPercent(A.percentParent,l),A.parent=b.getPtLabel(p)),A.percentEntry=d/b.getValue(e),A.percentEntryLabel=b.formatPercent(A.percentEntry,l),A.entry=b.getPtLabel(e),A.percentRoot=d/b.getValue(f),A.percentRootLabel=b.formatPercent(A.percentRoot,l),A.root=b.getPtLabel(f),u.hasOwnProperty("color")&&(A.color=u.color);var M=s.castOption(r,u.i,"text");return(s.isValidTextValue(M)||""===M)&&(A.text=M),A.customdata=s.castOption(r,u.i,"customdata"),s.texttemplateString(k,A,i._d3locale,A,r._meta||{})}},{"../../components/drawing":388,"../../lib":503,"../../lib/svg_text_utils":529,"../bar/style":662,"../bar/uniform_text":664,"../pie/helpers":906,"../pie/plot":910,"./constants":1052,"./fx":1054,"./helpers":1055,"./style":1060,"@plotly/d3":58,"d3-hierarchy":115,"d3-interpolate":116}],1060:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../components/color"),a=t("../../lib"),o=t("../bar/uniform_text").resizeText;function s(t,e,r){var n=e.data.data,o=!e.children,s=n.i,l=a.castOption(r,s,"marker.line.color")||i.defaultLine,c=a.castOption(r,s,"marker.line.width")||0;t.style("stroke-width",c).call(i.fill,n.color).call(i.stroke,l).style("opacity",o?r.leaf.opacity:null)}e.exports={style:function(t){var e=t._fullLayout._sunburstlayer.selectAll(".trace");o(t,e,"sunburst"),e.each((function(t){var e=n.select(this),r=t[0].trace;e.style("opacity",r.opacity),e.selectAll("path.surface").each((function(t){n.select(this).call(s,t,r)}))}))},styleOne:s}},{"../../components/color":366,"../../lib":503,"../bar/uniform_text":664,"@plotly/d3":58}],1061:[function(t,e,r){"use strict";var n=t("../../components/color"),i=t("../../components/colorscale/attributes"),a=t("../../plots/cartesian/axis_format_attributes").axisHoverFormat,o=t("../../plots/template_attributes").hovertemplateAttrs,s=t("../../plots/attributes"),l=t("../../lib/extend").extendFlat,c=t("../../plot_api/edit_types").overrideAll;function u(t){return{show:{valType:"boolean",dflt:!1},start:{valType:"number",dflt:null,editType:"plot"},end:{valType:"number",dflt:null,editType:"plot"},size:{valType:"number",dflt:null,min:0,editType:"plot"},project:{x:{valType:"boolean",dflt:!1},y:{valType:"boolean",dflt:!1},z:{valType:"boolean",dflt:!1}},color:{valType:"color",dflt:n.defaultLine},usecolormap:{valType:"boolean",dflt:!1},width:{valType:"number",min:1,max:16,dflt:2},highlight:{valType:"boolean",dflt:!0},highlightcolor:{valType:"color",dflt:n.defaultLine},highlightwidth:{valType:"number",min:1,max:16,dflt:2}}}var f=e.exports=c(l({z:{valType:"data_array"},x:{valType:"data_array"},y:{valType:"data_array"},text:{valType:"string",dflt:"",arrayOk:!0},hovertext:{valType:"string",dflt:"",arrayOk:!0},hovertemplate:o(),xhoverformat:a("x"),yhoverformat:a("y"),zhoverformat:a("z"),connectgaps:{valType:"boolean",dflt:!1,editType:"calc"},surfacecolor:{valType:"data_array"}},i("",{colorAttr:"z or surfacecolor",showScaleDflt:!0,autoColorDflt:!1,editTypeOverride:"calc"}),{contours:{x:u(),y:u(),z:u()},hidesurface:{valType:"boolean",dflt:!1},lightposition:{x:{valType:"number",min:-1e5,max:1e5,dflt:10},y:{valType:"number",min:-1e5,max:1e5,dflt:1e4},z:{valType:"number",min:-1e5,max:1e5,dflt:0}},lighting:{ambient:{valType:"number",min:0,max:1,dflt:.8},diffuse:{valType:"number",min:0,max:1,dflt:.8},specular:{valType:"number",min:0,max:2,dflt:.05},roughness:{valType:"number",min:0,max:1,dflt:.5},fresnel:{valType:"number",min:0,max:5,dflt:.2}},opacity:{valType:"number",min:0,max:1,dflt:1},opacityscale:{valType:"any",editType:"calc"},_deprecated:{zauto:l({},i.zauto,{}),zmin:l({},i.zmin,{}),zmax:l({},i.zmax,{})},hoverinfo:l({},s.hoverinfo),showlegend:l({},s.showlegend,{dflt:!1})}),"calc","nested");f.x.editType=f.y.editType=f.z.editType="calc+clearAxisTypes",f.transforms=void 0},{"../../components/color":366,"../../components/colorscale/attributes":373,"../../lib/extend":493,"../../plot_api/edit_types":536,"../../plots/attributes":550,"../../plots/cartesian/axis_format_attributes":557,"../../plots/template_attributes":633}],1062:[function(t,e,r){"use strict";var n=t("../../components/colorscale/calc");e.exports=function(t,e){e.surfacecolor?n(t,e,{vals:e.surfacecolor,containerStr:"",cLetter:"c"}):n(t,e,{vals:e.z,containerStr:"",cLetter:"c"})}},{"../../components/colorscale/calc":374}],1063:[function(t,e,r){"use strict";var n=t("../../../stackgl_modules").gl_surface3d,i=t("../../../stackgl_modules").ndarray,a=t("../../../stackgl_modules").ndarray_linear_interpolate.d2,o=t("../heatmap/interp2d"),s=t("../heatmap/find_empties"),l=t("../../lib").isArrayOrTypedArray,c=t("../../lib/gl_format_color").parseColorScale,u=t("../../lib/str2rgbarray"),f=t("../../components/colorscale").extractOpts;function h(t,e,r){this.scene=t,this.uid=r,this.surface=e,this.data=null,this.showContour=[!1,!1,!1],this.contourStart=[null,null,null],this.contourEnd=[null,null,null],this.contourSize=[0,0,0],this.minValues=[1/0,1/0,1/0],this.maxValues=[-1/0,-1/0,-1/0],this.dataScaleX=1,this.dataScaleY=1,this.refineData=!0,this.objectOffset=[0,0,0]}var p=h.prototype;p.getXat=function(t,e,r,n){var i=l(this.data.x)?l(this.data.x[0])?this.data.x[e][t]:this.data.x[t]:t;return void 0===r?i:n.d2l(i,0,r)},p.getYat=function(t,e,r,n){var i=l(this.data.y)?l(this.data.y[0])?this.data.y[e][t]:this.data.y[e]:e;return void 0===r?i:n.d2l(i,0,r)},p.getZat=function(t,e,r,n){var i=this.data.z[e][t];return null===i&&this.data.connectgaps&&this.data._interpolatedZ&&(i=this.data._interpolatedZ[e][t]),void 0===r?i:n.d2l(i,0,r)},p.handlePick=function(t){if(t.object===this.surface){var e=(t.data.index[0]-1)/this.dataScaleX-1,r=(t.data.index[1]-1)/this.dataScaleY-1,n=Math.max(Math.min(Math.round(e),this.data.z[0].length-1),0),i=Math.max(Math.min(Math.round(r),this.data._ylength-1),0);t.index=[n,i],t.traceCoordinate=[this.getXat(n,i),this.getYat(n,i),this.getZat(n,i)],t.dataCoordinate=[this.getXat(n,i,this.data.xcalendar,this.scene.fullSceneLayout.xaxis),this.getYat(n,i,this.data.ycalendar,this.scene.fullSceneLayout.yaxis),this.getZat(n,i,this.data.zcalendar,this.scene.fullSceneLayout.zaxis)];for(var a=0;a<3;a++){var o=t.dataCoordinate[a];null!=o&&(t.dataCoordinate[a]*=this.scene.dataScale[a])}var s=this.data.hovertext||this.data.text;return Array.isArray(s)&&s[i]&&void 0!==s[i][n]?t.textLabel=s[i][n]:t.textLabel=s||"",t.data.dataCoordinate=t.dataCoordinate.slice(),this.surface.highlight(t.data),this.scene.glplot.spikes.position=t.dataCoordinate,!0}};var d=[2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997,1009,1013,1019,1021,1031,1033,1039,1049,1051,1061,1063,1069,1087,1091,1093,1097,1103,1109,1117,1123,1129,1151,1153,1163,1171,1181,1187,1193,1201,1213,1217,1223,1229,1231,1237,1249,1259,1277,1279,1283,1289,1291,1297,1301,1303,1307,1319,1321,1327,1361,1367,1373,1381,1399,1409,1423,1427,1429,1433,1439,1447,1451,1453,1459,1471,1481,1483,1487,1489,1493,1499,1511,1523,1531,1543,1549,1553,1559,1567,1571,1579,1583,1597,1601,1607,1609,1613,1619,1621,1627,1637,1657,1663,1667,1669,1693,1697,1699,1709,1721,1723,1733,1741,1747,1753,1759,1777,1783,1787,1789,1801,1811,1823,1831,1847,1861,1867,1871,1873,1877,1879,1889,1901,1907,1913,1931,1933,1949,1951,1973,1979,1987,1993,1997,1999,2003,2011,2017,2027,2029,2039,2053,2063,2069,2081,2083,2087,2089,2099,2111,2113,2129,2131,2137,2141,2143,2153,2161,2179,2203,2207,2213,2221,2237,2239,2243,2251,2267,2269,2273,2281,2287,2293,2297,2309,2311,2333,2339,2341,2347,2351,2357,2371,2377,2381,2383,2389,2393,2399,2411,2417,2423,2437,2441,2447,2459,2467,2473,2477,2503,2521,2531,2539,2543,2549,2551,2557,2579,2591,2593,2609,2617,2621,2633,2647,2657,2659,2663,2671,2677,2683,2687,2689,2693,2699,2707,2711,2713,2719,2729,2731,2741,2749,2753,2767,2777,2789,2791,2797,2801,2803,2819,2833,2837,2843,2851,2857,2861,2879,2887,2897,2903,2909,2917,2927,2939,2953,2957,2963,2969,2971,2999];function m(t,e){if(t0){r=d[n];break}return r}function y(t,e){if(!(t<1||e<1)){for(var r=g(t),n=g(e),i=1,a=0;a_;)r--,r/=v(r),++r1?n:1},p.refineCoords=function(t){for(var e=this.dataScaleX,r=this.dataScaleY,n=t[0].shape[0],a=t[0].shape[1],o=0|Math.floor(t[0].shape[0]*e+1),s=0|Math.floor(t[0].shape[1]*r+1),l=1+n+1,c=1+a+1,u=i(new Float32Array(l*c),[l,c]),f=[1/e,0,0,0,1/r,0,0,0,1],h=0;h0&&null!==this.contourStart[t]&&null!==this.contourEnd[t]&&this.contourEnd[t]>this.contourStart[t]))for(i[t]=!0,e=this.contourStart[t];ea&&(this.minValues[e]=a),this.maxValues[e]",maxDimensionCount:60,overdrag:45,releaseTransitionDuration:120,releaseTransitionEase:"cubic-out",scrollbarCaptureWidth:18,scrollbarHideDelay:1e3,scrollbarHideDuration:1e3,scrollbarOffset:5,scrollbarWidth:8,transitionDuration:100,transitionEase:"cubic-out",uplift:5,wrapSpacer:" ",wrapSplitCharacter:" ",cn:{table:"table",tableControlView:"table-control-view",scrollBackground:"scroll-background",yColumn:"y-column",columnBlock:"column-block",scrollAreaClip:"scroll-area-clip",scrollAreaClipRect:"scroll-area-clip-rect",columnBoundary:"column-boundary",columnBoundaryClippath:"column-boundary-clippath",columnBoundaryRect:"column-boundary-rect",columnCells:"column-cells",columnCell:"column-cell",cellRect:"cell-rect",cellText:"cell-text",cellTextHolder:"cell-text-holder",scrollbarKit:"scrollbar-kit",scrollbar:"scrollbar",scrollbarSlider:"scrollbar-slider",scrollbarGlyph:"scrollbar-glyph",scrollbarCaptureZone:"scrollbar-capture-zone"}}},{}],1070:[function(t,e,r){"use strict";var n=t("./constants"),i=t("../../lib/extend").extendFlat,a=t("fast-isnumeric");function o(t){if(Array.isArray(t)){for(var e=0,r=0;r=e||c===t.length-1)&&(n[i]=o,o.key=l++,o.firstRowIndex=s,o.lastRowIndex=c,o={firstRowIndex:null,lastRowIndex:null,rows:[]},i+=a,s=c+1,a=0);return n}e.exports=function(t,e){var r=l(e.cells.values),p=function(t){return t.slice(e.header.values.length,t.length)},d=l(e.header.values);d.length&&!d[0].length&&(d[0]=[""],d=l(d));var m=d.concat(p(r).map((function(){return c((d[0]||[""]).length)}))),g=e.domain,v=Math.floor(t._fullLayout._size.w*(g.x[1]-g.x[0])),y=Math.floor(t._fullLayout._size.h*(g.y[1]-g.y[0])),x=e.header.values.length?m[0].map((function(){return e.header.height})):[n.emptyHeaderHeight],b=r.length?r[0].map((function(){return e.cells.height})):[],_=x.reduce(s,0),w=h(b,y-_+n.uplift),T=f(h(x,_),[]),k=f(w,T),A={},M=e._fullInput.columnorder.concat(p(r.map((function(t,e){return e})))),S=m.map((function(t,r){var n=Array.isArray(e.columnwidth)?e.columnwidth[Math.min(r,e.columnwidth.length-1)]:e.columnwidth;return a(n)?Number(n):1})),E=S.reduce(s,0);S=S.map((function(t){return t/E*v}));var L=Math.max(o(e.header.line.width),o(e.cells.line.width)),C={key:e.uid+t._context.staticPlot,translateX:g.x[0]*t._fullLayout._size.w,translateY:t._fullLayout._size.h*(1-g.y[1]),size:t._fullLayout._size,width:v,maxLineWidth:L,height:y,columnOrder:M,groupHeight:y,rowBlocks:k,headerRowBlocks:T,scrollY:0,cells:i({},e.cells,{values:r}),headerCells:i({},e.header,{values:m}),gdColumns:m.map((function(t){return t[0]})),gdColumnsOriginalOrder:m.map((function(t){return t[0]})),prevPages:[0,0],scrollbarState:{scrollbarScrollInProgress:!1},columns:m.map((function(t,e){var r=A[t];return A[t]=(r||0)+1,{key:t+"__"+A[t],label:t,specIndex:e,xIndex:M[e],xScale:u,x:void 0,calcdata:void 0,columnWidth:S[e]}}))};return C.columns.forEach((function(t){t.calcdata=C,t.x=u(t)})),C}},{"../../lib/extend":493,"./constants":1069,"fast-isnumeric":190}],1071:[function(t,e,r){"use strict";var n=t("../../lib/extend").extendFlat;r.splitToPanels=function(t){var e=[0,0],r=n({},t,{key:"header",type:"header",page:0,prevPages:e,currentRepaint:[null,null],dragHandle:!0,values:t.calcdata.headerCells.values[t.specIndex],rowBlocks:t.calcdata.headerRowBlocks,calcdata:n({},t.calcdata,{cells:t.calcdata.headerCells})});return[n({},t,{key:"cells1",type:"cells",page:0,prevPages:e,currentRepaint:[null,null],dragHandle:!1,values:t.calcdata.cells.values[t.specIndex],rowBlocks:t.calcdata.rowBlocks}),n({},t,{key:"cells2",type:"cells",page:1,prevPages:e,currentRepaint:[null,null],dragHandle:!1,values:t.calcdata.cells.values[t.specIndex],rowBlocks:t.calcdata.rowBlocks}),r]},r.splitToCells=function(t){var e=function(t){var e=t.rowBlocks[t.page],r=e?e.rows[0].rowIndex:0,n=e?r+e.rows.length:0;return[r,n]}(t);return(t.values||[]).slice(e[0],e[1]).map((function(r,n){return{keyWithinBlock:n+("string"==typeof r&&r.match(/[<$&> ]/)?"_keybuster_"+Math.random():""),key:e[0]+n,column:t,calcdata:t.calcdata,page:t.page,rowBlocks:t.rowBlocks,value:r}}))}},{"../../lib/extend":493}],1072:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("./attributes"),a=t("../../plots/domain").defaults;e.exports=function(t,e,r,o){function s(r,a){return n.coerce(t,e,i,r,a)}a(e,o,s),s("columnwidth"),s("header.values"),s("header.format"),s("header.align"),s("header.prefix"),s("header.suffix"),s("header.height"),s("header.line.width"),s("header.line.color"),s("header.fill.color"),n.coerceFont(s,"header.font",n.extendFlat({},o.font)),function(t,e){for(var r=t.columnorder||[],n=t.header.values.length,i=r.slice(0,n),a=i.slice().sort((function(t,e){return t-e})),o=i.map((function(t){return a.indexOf(t)})),s=o.length;s/i),l=!o||s;t.mayHaveMarkup=o&&i.match(/[<&>]/);var c,u="string"==typeof(c=i)&&c.match(n.latexCheck);t.latex=u;var f,h,p=u?"":T(t.calcdata.cells.prefix,e,r)||"",d=u?"":T(t.calcdata.cells.suffix,e,r)||"",m=u?null:T(t.calcdata.cells.format,e,r)||null,g=p+(m?a(m)(t.value):t.value)+d;if(t.wrappingNeeded=!t.wrapped&&!l&&!u&&(f=w(g)),t.cellHeightMayIncrease=s||u||t.mayHaveMarkup||(void 0===f?w(g):f),t.needsConvertToTspans=t.mayHaveMarkup||t.wrappingNeeded||t.latex,t.wrappingNeeded){var v=(" "===n.wrapSplitCharacter?g.replace(/i&&n.push(a),i+=l}return n}(i,l,s);1===c.length&&(c[0]===i.length-1?c.unshift(c[0]-1):c.push(c[0]+1)),c[0]%2&&c.reverse(),e.each((function(t,e){t.page=c[e],t.scrollY=l})),e.attr("transform",(function(t){var e=D(t.rowBlocks,t.page)-t.scrollY;return u(0,e)})),t&&(C(t,r,e,c,n.prevPages,n,0),C(t,r,e,c,n.prevPages,n,1),x(r,t))}}function L(t,e,r,a){return function(o){var s=o.calcdata?o.calcdata:o,l=e.filter((function(t){return s.key===t.key})),c=r||s.scrollbarState.dragMultiplier,u=s.scrollY;s.scrollY=void 0===a?s.scrollY+c*i.event.dy:a;var f=l.selectAll("."+n.cn.yColumn).selectAll("."+n.cn.columnBlock).filter(A);return E(t,f,l),s.scrollY===u}}function C(t,e,r,n,i,a,o){n[o]!==i[o]&&(clearTimeout(a.currentRepaint[o]),a.currentRepaint[o]=setTimeout((function(){var a=r.filter((function(t,e){return e===o&&n[e]!==i[e]}));b(t,e,a,r),i[o]=n[o]})))}function P(t,e,r,a){return function(){var o=i.select(e.parentNode);o.each((function(t){var e=t.fragments;o.selectAll("tspan.line").each((function(t,r){e[r].width=this.getComputedTextLength()}));var r,i,a=e[e.length-1].width,s=e.slice(0,-1),l=[],c=0,u=t.column.columnWidth-2*n.cellPad;for(t.value="";s.length;)c+(i=(r=s.shift()).width+a)>u&&(t.value+=l.join(n.wrapSpacer)+n.lineBreaker,l=[],c=0),l.push(r.text),c+=i;c&&(t.value+=l.join(n.wrapSpacer)),t.wrapped=!0})),o.selectAll("tspan.line").remove(),_(o.select("."+n.cn.cellText),r,t,a),i.select(e.parentNode.parentNode).call(z)}}function I(t,e,r,a,o){return function(){if(!o.settledY){var s=i.select(e.parentNode),l=B(o),c=o.key-l.firstRowIndex,f=l.rows[c].rowHeight,h=o.cellHeightMayIncrease?e.parentNode.getBoundingClientRect().height+2*n.cellPad:f,p=Math.max(h,f);p-l.rows[c].rowHeight&&(l.rows[c].rowHeight=p,t.selectAll("."+n.cn.columnCell).call(z),E(null,t.filter(A),0),x(r,a,!0)),s.attr("transform",(function(){var t=this.parentNode.getBoundingClientRect(),e=i.select(this.parentNode).select("."+n.cn.cellRect).node().getBoundingClientRect(),r=this.transform.baseVal.consolidate(),a=e.top-t.top+(r?r.matrix.f:n.cellPad);return u(O(o,i.select(this.parentNode).select("."+n.cn.cellTextHolder).node().getBoundingClientRect().width),a)})),o.settledY=!0}}}function O(t,e){switch(t.align){case"left":return n.cellPad;case"right":return t.column.columnWidth-(e||0)-n.cellPad;case"center":return(t.column.columnWidth-(e||0))/2;default:return n.cellPad}}function z(t){t.attr("transform",(function(t){var e=t.rowBlocks[0].auxiliaryBlocks.reduce((function(t,e){return t+R(e,1/0)}),0),r=R(B(t),t.key);return u(0,r+e)})).selectAll("."+n.cn.cellRect).attr("height",(function(t){return(e=B(t),r=t.key,e.rows[r-e.firstRowIndex]).rowHeight;var e,r}))}function D(t,e){for(var r=0,n=e-1;n>=0;n--)r+=F(t[n]);return r}function R(t,e){for(var r=0,n=0;n","<","|","/","\\"],dflt:">",editType:"plot"},thickness:{valType:"number",min:12,editType:"plot"},textfont:u({},s.textfont,{}),editType:"calc"},text:s.text,textinfo:l.textinfo,texttemplate:i({editType:"plot"},{keys:c.eventDataKeys.concat(["label","value"])}),hovertext:s.hovertext,hoverinfo:l.hoverinfo,hovertemplate:n({},{keys:c.eventDataKeys}),textfont:s.textfont,insidetextfont:s.insidetextfont,outsidetextfont:u({},s.outsidetextfont,{}),textposition:{valType:"enumerated",values:["top left","top center","top right","middle left","middle center","middle right","bottom left","bottom center","bottom right"],dflt:"top left",editType:"plot"},sort:s.sort,root:l.root,domain:o({name:"treemap",trace:!0,editType:"calc"})}},{"../../components/colorscale/attributes":373,"../../lib/extend":493,"../../plots/domain":584,"../../plots/template_attributes":633,"../pie/attributes":901,"../sunburst/attributes":1049,"./constants":1078}],1076:[function(t,e,r){"use strict";var n=t("../../plots/plots");r.name="treemap",r.plot=function(t,e,i,a){n.plotBasePlot(r.name,t,e,i,a)},r.clean=function(t,e,i,a){n.cleanBasePlot(r.name,t,e,i,a)}},{"../../plots/plots":619}],1077:[function(t,e,r){"use strict";var n=t("../sunburst/calc");r.calc=function(t,e){return n.calc(t,e)},r.crossTraceCalc=function(t){return n._runCrossTraceCalc("treemap",t)}},{"../sunburst/calc":1051}],1078:[function(t,e,r){"use strict";e.exports={CLICK_TRANSITION_TIME:750,CLICK_TRANSITION_EASING:"poly",eventDataKeys:["currentPath","root","entry","percentRoot","percentEntry","percentParent"],gapWithPathbar:1}},{}],1079:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("./attributes"),a=t("../../components/color"),o=t("../../plots/domain").defaults,s=t("../bar/defaults").handleText,l=t("../bar/constants").TEXTPAD,c=t("../../components/colorscale"),u=c.hasColorscale,f=c.handleDefaults;e.exports=function(t,e,r,c){function h(r,a){return n.coerce(t,e,i,r,a)}var p=h("labels"),d=h("parents");if(p&&p.length&&d&&d.length){var m=h("values");m&&m.length?h("branchvalues"):h("count"),h("level"),h("maxdepth"),"squarify"===h("tiling.packing")&&h("tiling.squarifyratio"),h("tiling.flip"),h("tiling.pad");var g=h("text");h("texttemplate"),e.texttemplate||h("textinfo",Array.isArray(g)?"text+label":"label"),h("hovertext"),h("hovertemplate");var v=h("pathbar.visible");s(t,e,c,h,"auto",{hasPathbar:v,moduleHasSelected:!1,moduleHasUnselected:!1,moduleHasConstrain:!1,moduleHasCliponaxis:!1,moduleHasTextangle:!1,moduleHasInsideanchor:!1}),h("textposition");var y=-1!==e.textposition.indexOf("bottom");h("marker.line.width")&&h("marker.line.color",c.paper_bgcolor);var x=h("marker.colors");(e._hasColorscale=u(t,"marker","colors")||(t.marker||{}).coloraxis)?f(t,e,c,h,{prefix:"marker.",cLetter:"c"}):h("marker.depthfade",!(x||[]).length);var b=2*e.textfont.size;h("marker.pad.t",y?b/4:b),h("marker.pad.l",b/4),h("marker.pad.r",b/4),h("marker.pad.b",y?b:b/4),e._hovered={marker:{line:{width:2,color:a.contrast(c.paper_bgcolor)}}},v&&(h("pathbar.thickness",e.pathbar.textfont.size+2*l),h("pathbar.side"),h("pathbar.edgeshape")),h("sort"),h("root.color"),o(e,c,h),e._length=null}else e.visible=!1}},{"../../components/color":366,"../../components/colorscale":378,"../../lib":503,"../../plots/domain":584,"../bar/constants":650,"../bar/defaults":652,"./attributes":1075}],1080:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../sunburst/helpers"),a=t("../bar/uniform_text").clearMinTextSize,o=t("../bar/style").resizeText,s=t("./plot_one");e.exports=function(t,e,r,l,c){var u,f,h=c.type,p=c.drawDescendants,d=t._fullLayout,m=d["_"+h+"layer"],g=!r;(a(h,d),(u=m.selectAll("g.trace."+h).data(e,(function(t){return t[0].trace.uid}))).enter().append("g").classed("trace",!0).classed(h,!0),u.order(),!d.uniformtext.mode&&i.hasTransition(r))?(l&&(f=l()),n.transition().duration(r.duration).ease(r.easing).each("end",(function(){f&&f()})).each("interrupt",(function(){f&&f()})).each((function(){m.selectAll("g.trace").each((function(e){s(t,e,this,r,p)}))}))):(u.each((function(e){s(t,e,this,r,p)})),d.uniformtext.mode&&o(t,m.selectAll(".trace"),h));g&&u.exit().remove()}},{"../bar/style":662,"../bar/uniform_text":664,"../sunburst/helpers":1055,"./plot_one":1089,"@plotly/d3":58}],1081:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../lib"),a=t("../../components/drawing"),o=t("../../lib/svg_text_utils"),s=t("./partition"),l=t("./style").styleOne,c=t("./constants"),u=t("../sunburst/helpers"),f=t("../sunburst/fx");e.exports=function(t,e,r,h,p){var d=p.barDifY,m=p.width,g=p.height,v=p.viewX,y=p.viewY,x=p.pathSlice,b=p.toMoveInsideSlice,_=p.strTransform,w=p.hasTransition,T=p.handleSlicesExit,k=p.makeUpdateSliceInterpolator,A=p.makeUpdateTextInterpolator,M={},S=t._fullLayout,E=e[0],L=E.trace,C=E.hierarchy,P=m/L._entryDepth,I=u.listPath(r.data,"id"),O=s(C.copy(),[m,g],{packing:"dice",pad:{inner:0,top:0,left:0,right:0,bottom:0}}).descendants();(O=O.filter((function(t){var e=I.indexOf(t.data.id);return-1!==e&&(t.x0=P*e,t.x1=P*(e+1),t.y0=d,t.y1=d+g,t.onPathbar=!0,!0)}))).reverse(),(h=h.data(O,u.getPtId)).enter().append("g").classed("pathbar",!0),T(h,!0,M,[m,g],x),h.order();var z=h;w&&(z=z.transition().each("end",(function(){var e=n.select(this);u.setSliceCursor(e,t,{hideOnRoot:!1,hideOnLeaves:!1,isTransitioning:!1})}))),z.each((function(s){s._x0=v(s.x0),s._x1=v(s.x1),s._y0=y(s.y0),s._y1=y(s.y1),s._hoverX=v(s.x1-Math.min(m,g)/2),s._hoverY=y(s.y1-g/2);var h=n.select(this),p=i.ensureSingle(h,"path","surface",(function(t){t.style("pointer-events","all")}));w?p.transition().attrTween("d",(function(t){var e=k(t,!0,M,[m,g]);return function(t){return x(e(t))}})):p.attr("d",x),h.call(f,r,t,e,{styleOne:l,eventDataKeys:c.eventDataKeys,transitionTime:c.CLICK_TRANSITION_TIME,transitionEasing:c.CLICK_TRANSITION_EASING}).call(u.setSliceCursor,t,{hideOnRoot:!1,hideOnLeaves:!1,isTransitioning:t._transitioning}),p.call(l,s,L,{hovered:!1}),s._text=(u.getPtLabel(s)||"").split("
").join(" ")||"";var d=i.ensureSingle(h,"g","slicetext"),T=i.ensureSingle(d,"text","",(function(t){t.attr("data-notex",1)})),E=i.ensureUniformFontSize(t,u.determineTextFont(L,s,S.font,{onPathbar:!0}));T.text(s._text||" ").classed("slicetext",!0).attr("text-anchor","start").call(a.font,E).call(o.convertToTspans,t),s.textBB=a.bBox(T.node()),s.transform=b(s,{fontSize:E.size,onPathbar:!0}),s.transform.fontSize=E.size,w?T.transition().attrTween("transform",(function(t){var e=A(t,!0,M,[m,g]);return function(t){return _(e(t))}})):T.attr("transform",_(s))}))}},{"../../components/drawing":388,"../../lib":503,"../../lib/svg_text_utils":529,"../sunburst/fx":1054,"../sunburst/helpers":1055,"./constants":1078,"./partition":1087,"./style":1090,"@plotly/d3":58}],1082:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../lib"),a=t("../../components/drawing"),o=t("../../lib/svg_text_utils"),s=t("./partition"),l=t("./style").styleOne,c=t("./constants"),u=t("../sunburst/helpers"),f=t("../sunburst/fx"),h=t("../sunburst/plot").formatSliceLabel;e.exports=function(t,e,r,p,d){var m=d.width,g=d.height,v=d.viewX,y=d.viewY,x=d.pathSlice,b=d.toMoveInsideSlice,_=d.strTransform,w=d.hasTransition,T=d.handleSlicesExit,k=d.makeUpdateSliceInterpolator,A=d.makeUpdateTextInterpolator,M=d.prevEntry,S=t._fullLayout,E=e[0].trace,L=-1!==E.textposition.indexOf("left"),C=-1!==E.textposition.indexOf("right"),P=-1!==E.textposition.indexOf("bottom"),I=!P&&!E.marker.pad.t||P&&!E.marker.pad.b,O=s(r,[m,g],{packing:E.tiling.packing,squarifyratio:E.tiling.squarifyratio,flipX:E.tiling.flip.indexOf("x")>-1,flipY:E.tiling.flip.indexOf("y")>-1,pad:{inner:E.tiling.pad,top:E.marker.pad.t,left:E.marker.pad.l,right:E.marker.pad.r,bottom:E.marker.pad.b}}).descendants(),z=1/0,D=-1/0;O.forEach((function(t){var e=t.depth;e>=E._maxDepth?(t.x0=t.x1=(t.x0+t.x1)/2,t.y0=t.y1=(t.y0+t.y1)/2):(z=Math.min(z,e),D=Math.max(D,e))})),p=p.data(O,u.getPtId),E._maxVisibleLayers=isFinite(D)?D-z+1:0,p.enter().append("g").classed("slice",!0),T(p,!1,{},[m,g],x),p.order();var R=null;if(w&&M){var F=u.getPtId(M);p.each((function(t){null===R&&u.getPtId(t)===F&&(R={x0:t.x0,x1:t.x1,y0:t.y0,y1:t.y1})}))}var B=function(){return R||{x0:0,x1:m,y0:0,y1:g}},N=p;return w&&(N=N.transition().each("end",(function(){var e=n.select(this);u.setSliceCursor(e,t,{hideOnRoot:!0,hideOnLeaves:!1,isTransitioning:!1})}))),N.each((function(s){var p=u.isHeader(s,E);s._x0=v(s.x0),s._x1=v(s.x1),s._y0=y(s.y0),s._y1=y(s.y1),s._hoverX=v(s.x1-E.marker.pad.r),s._hoverY=y(P?s.y1-E.marker.pad.b/2:s.y0+E.marker.pad.t/2);var d=n.select(this),T=i.ensureSingle(d,"path","surface",(function(t){t.style("pointer-events","all")}));w?T.transition().attrTween("d",(function(t){var e=k(t,!1,B(),[m,g]);return function(t){return x(e(t))}})):T.attr("d",x),d.call(f,r,t,e,{styleOne:l,eventDataKeys:c.eventDataKeys,transitionTime:c.CLICK_TRANSITION_TIME,transitionEasing:c.CLICK_TRANSITION_EASING}).call(u.setSliceCursor,t,{isTransitioning:t._transitioning}),T.call(l,s,E,{hovered:!1}),s.x0===s.x1||s.y0===s.y1?s._text="":s._text=p?I?"":u.getPtLabel(s)||"":h(s,r,E,e,S)||"";var M=i.ensureSingle(d,"g","slicetext"),O=i.ensureSingle(M,"text","",(function(t){t.attr("data-notex",1)})),z=i.ensureUniformFontSize(t,u.determineTextFont(E,s,S.font));O.text(s._text||" ").classed("slicetext",!0).attr("text-anchor",C?"end":L||p?"start":"middle").call(a.font,z).call(o.convertToTspans,t),s.textBB=a.bBox(O.node()),s.transform=b(s,{fontSize:z.size,isHeader:p}),s.transform.fontSize=z.size,w?O.transition().attrTween("transform",(function(t){var e=A(t,!1,B(),[m,g]);return function(t){return _(e(t))}})):O.attr("transform",_(s))})),R}},{"../../components/drawing":388,"../../lib":503,"../../lib/svg_text_utils":529,"../sunburst/fx":1054,"../sunburst/helpers":1055,"../sunburst/plot":1059,"./constants":1078,"./partition":1087,"./style":1090,"@plotly/d3":58}],1083:[function(t,e,r){"use strict";e.exports=function t(e,r,n){var i;n.swapXY&&(i=e.x0,e.x0=e.y0,e.y0=i,i=e.x1,e.x1=e.y1,e.y1=i),n.flipX&&(i=e.x0,e.x0=r[0]-e.x1,e.x1=r[0]-i),n.flipY&&(i=e.y0,e.y0=r[1]-e.y1,e.y1=r[1]-i);var a=e.children;if(a)for(var o=0;o-1?C+O:-(I+O):0,D={x0:P,x1:P,y0:z,y1:z+I},R=function(t,e,r){var n=v.tiling.pad,i=function(t){return t-n<=e.x0},a=function(t){return t+n>=e.x1},o=function(t){return t-n<=e.y0},s=function(t){return t+n>=e.y1};return t.x0===e.x0&&t.x1===e.x1&&t.y0===e.y0&&t.y1===e.y1?{x0:t.x0,x1:t.x1,y0:t.y0,y1:t.y1}:{x0:i(t.x0-n)?0:a(t.x0-n)?r[0]:t.x0,x1:i(t.x1+n)?0:a(t.x1+n)?r[0]:t.x1,y0:o(t.y0-n)?0:s(t.y0-n)?r[1]:t.y0,y1:o(t.y1+n)?0:s(t.y1+n)?r[1]:t.y1}},F=null,B={},N={},j=null,U=function(t,e){return e?B[h(t)]:N[h(t)]},V=function(t,e,r,n){if(e)return B[h(x)]||D;var i=N[v.level]||r;return function(t){return t.data.depth-b.data.depth=(n-=(y?g:g.r)-s)){var x=(r+n)/2;r=x,n=x}var b;f?i<(b=a-(y?g:g.b))&&b"===tt?(l.x-=a,c.x-=a,u.x-=a,f.x-=a):"/"===tt?(u.x-=a,f.x-=a,o.x-=a/2,s.x-=a/2):"\\"===tt?(l.x-=a,c.x-=a,o.x-=a/2,s.x-=a/2):"<"===tt&&(o.x-=a,s.x-=a),$(l),$(f),$(o),$(c),$(u),$(s),"M"+K(l.x,l.y)+"L"+K(c.x,c.y)+"L"+K(s.x,s.y)+"L"+K(u.x,u.y)+"L"+K(f.x,f.y)+"L"+K(o.x,o.y)+"Z"},toMoveInsideSlice:et,makeUpdateSliceInterpolator:nt,makeUpdateTextInterpolator:it,handleSlicesExit:at,hasTransition:A,strTransform:ot}):w.remove()}},{"../../lib":503,"../bar/constants":650,"../bar/plot":659,"../bar/uniform_text":664,"../sunburst/helpers":1055,"./constants":1078,"./draw_ancestors":1081,"@plotly/d3":58,"d3-interpolate":116}],1090:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../components/color"),a=t("../../lib"),o=t("../sunburst/helpers"),s=t("../bar/uniform_text").resizeText;function l(t,e,r,n){var s,l,c=(n||{}).hovered,u=e.data.data,f=u.i,h=u.color,p=o.isHierarchyRoot(e),d=1;if(c)s=r._hovered.marker.line.color,l=r._hovered.marker.line.width;else if(p&&h===r.root.color)d=100,s="rgba(0,0,0,0)",l=0;else if(s=a.castOption(r,f,"marker.line.color")||i.defaultLine,l=a.castOption(r,f,"marker.line.width")||0,!r._hasColorscale&&!e.onPathbar){var m=r.marker.depthfade;if(m){var g,v=i.combine(i.addOpacity(r._backgroundColor,.75),h);if(!0===m){var y=o.getMaxDepth(r);g=isFinite(y)?o.isLeaf(e)?0:r._maxVisibleLayers-(e.data.depth-r._entryDepth):e.data.height+1}else g=e.data.depth-r._entryDepth,r._atRootLevel||g++;if(g>0)for(var x=0;x0){var x,b,_,w,T,k=t.xa,A=t.ya;"h"===p.orientation?(T=e,x="y",_=A,b="x",w=k):(T=r,x="x",_=k,b="y",w=A);var M=h[t.index];if(T>=M.span[0]&&T<=M.span[1]){var S=n.extendFlat({},t),E=w.c2p(T,!0),L=o.getKdeValue(M,p,T),C=o.getPositionOnKdePath(M,p,E),P=_._offset,I=_._length;S[x+"0"]=C[0],S[x+"1"]=C[1],S[b+"0"]=S[b+"1"]=E,S[b+"Label"]=b+": "+i.hoverLabelText(w,T,p[b+"hoverformat"])+", "+h[0].t.labels.kde+" "+L.toFixed(3),S.spikeDistance=y[0].spikeDistance;var O=x+"Spike";S[O]=y[0][O],y[0].spikeDistance=void 0,y[0][O]=void 0,S.hovertemplate=!1,v.push(S),(u={stroke:t.color})[x+"1"]=n.constrain(P+C[0],P,P+I),u[x+"2"]=n.constrain(P+C[1],P,P+I),u[b+"1"]=u[b+"2"]=w._offset+E}}m&&(v=v.concat(y))}-1!==d.indexOf("points")&&(c=a.hoverOnPoints(t,e,r));var z=f.selectAll(".violinline-"+p.uid).data(u?[0]:[]);return z.enter().append("line").classed("violinline-"+p.uid,!0).attr("stroke-width",1.5),z.exit().remove(),z.attr(u),"closest"===s?c?[c]:v:c?(v.push(c),v):v}},{"../../lib":503,"../../plots/cartesian/axes":554,"../box/hover":678,"./helpers":1095}],1097:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),layoutAttributes:t("./layout_attributes"),supplyDefaults:t("./defaults"),crossTraceDefaults:t("../box/defaults").crossTraceDefaults,supplyLayoutDefaults:t("./layout_defaults"),calc:t("./calc"),crossTraceCalc:t("./cross_trace_calc"),plot:t("./plot"),style:t("./style"),styleOnSelect:t("../scatter/style").styleOnSelect,hoverPoints:t("./hover"),selectPoints:t("../box/select"),moduleType:"trace",name:"violin",basePlotModule:t("../../plots/cartesian"),categories:["cartesian","svg","symbols","oriented","box-violin","showLegend","violinLayout","zoomScale"],meta:{}}},{"../../plots/cartesian":568,"../box/defaults":676,"../box/select":683,"../scatter/style":951,"./attributes":1091,"./calc":1092,"./cross_trace_calc":1093,"./defaults":1094,"./hover":1096,"./layout_attributes":1098,"./layout_defaults":1099,"./plot":1100,"./style":1101}],1098:[function(t,e,r){"use strict";var n=t("../box/layout_attributes"),i=t("../../lib").extendFlat;e.exports={violinmode:i({},n.boxmode,{}),violingap:i({},n.boxgap,{}),violingroupgap:i({},n.boxgroupgap,{})}},{"../../lib":503,"../box/layout_attributes":680}],1099:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("./layout_attributes"),a=t("../box/layout_defaults");e.exports=function(t,e,r){a._supply(t,e,r,(function(r,a){return n.coerce(t,e,i,r,a)}),"violin")}},{"../../lib":503,"../box/layout_defaults":681,"./layout_attributes":1098}],1100:[function(t,e,r){"use strict";var n=t("@plotly/d3"),i=t("../../lib"),a=t("../../components/drawing"),o=t("../box/plot"),s=t("../scatter/line_points"),l=t("./helpers");e.exports=function(t,e,r,c){var u=t._fullLayout,f=e.xaxis,h=e.yaxis;function p(t){var e=s(t,{xaxis:f,yaxis:h,connectGaps:!0,baseTolerance:.75,shape:"spline",simplify:!0,linearized:!0});return a.smoothopen(e[0],1)}i.makeTraceGroups(c,r,"trace violins").each((function(t){var r=n.select(this),a=t[0],s=a.t,c=a.trace;if(!0!==c.visible||s.empty)r.remove();else{var d=s.bPos,m=s.bdPos,g=e[s.valLetter+"axis"],v=e[s.posLetter+"axis"],y="both"===c.side,x=y||"positive"===c.side,b=y||"negative"===c.side,_=r.selectAll("path.violin").data(i.identity);_.enter().append("path").style("vector-effect","non-scaling-stroke").attr("class","violin"),_.exit().remove(),_.each((function(t){var e,r,i,a,o,l,f,h,_=n.select(this),w=t.density,T=w.length,k=v.c2l(t.pos+d,!0),A=v.l2p(k);if(c.width)e=s.maxKDE/m;else{var M=u._violinScaleGroupStats[c.scalegroup];e="count"===c.scalemode?M.maxKDE/m*(M.maxCount/t.pts.length):M.maxKDE/m}if(x){for(f=new Array(T),o=0;o")),u.color=function(t,e){var r=t[e.dir].marker,n=r.color,a=r.line.color,o=r.line.width;if(i(n))return n;if(i(a)&&o)return a}(h,g),[u]}function k(t){return n(m,t,h[d+"hoverformat"])}}},{"../../components/color":366,"../../constants/delta.js":473,"../../plots/cartesian/axes":554,"../bar/hover":655}],1113:[function(t,e,r){"use strict";e.exports={attributes:t("./attributes"),layoutAttributes:t("./layout_attributes"),supplyDefaults:t("./defaults").supplyDefaults,crossTraceDefaults:t("./defaults").crossTraceDefaults,supplyLayoutDefaults:t("./layout_defaults"),calc:t("./calc"),crossTraceCalc:t("./cross_trace_calc"),plot:t("./plot"),style:t("./style").style,hoverPoints:t("./hover"),eventData:t("./event_data"),selectPoints:t("../bar/select"),moduleType:"trace",name:"waterfall",basePlotModule:t("../../plots/cartesian"),categories:["bar-like","cartesian","svg","oriented","showLegend","zoomScale"],meta:{}}},{"../../plots/cartesian":568,"../bar/select":660,"./attributes":1106,"./calc":1107,"./cross_trace_calc":1109,"./defaults":1110,"./event_data":1111,"./hover":1112,"./layout_attributes":1114,"./layout_defaults":1115,"./plot":1116,"./style":1117}],1114:[function(t,e,r){"use strict";e.exports={waterfallmode:{valType:"enumerated",values:["group","overlay"],dflt:"group",editType:"calc"},waterfallgap:{valType:"number",min:0,max:1,editType:"calc"},waterfallgroupgap:{valType:"number",min:0,max:1,dflt:0,editType:"calc"}}},{}],1115:[function(t,e,r){"use strict";var n=t("../../lib"),i=t("./layout_attributes");e.exports=function(t,e,r){var a=!1;function o(r,a){return n.coerce(t,e,i,r,a)}for(var s=0;s0&&(g+=h?"M"+f[0]+","+d[1]+"V"+d[0]:"M"+f[1]+","+d[0]+"H"+f[0]),"between"!==p&&(r.isSum||s path").each((function(t){if(!t.isBlank){var e=s[t.dir].marker;n.select(this).call(a.fill,e.color).call(a.stroke,e.line.color).call(i.dashLine,e.line.dash,e.line.width).style("opacity",s.selectedpoints&&!t.selected?o:1)}})),c(r,s,t),r.selectAll(".lines").each((function(){var t=s.connector.line;i.lineGroupStyle(n.select(this).selectAll("path"),t.width,t.color,t.dash)}))}))}}},{"../../components/color":366,"../../components/drawing":388,"../../constants/interactions":478,"../bar/style":662,"../bar/uniform_text":664,"@plotly/d3":58}],1118:[function(t,e,r){"use strict";var n=t("../plots/cartesian/axes"),i=t("../lib"),a=t("../plot_api/plot_schema"),o=t("./helpers").pointsAccessorFunction,s=t("../constants/numerical").BADNUM;r.moduleType="transform",r.name="aggregate";var l=r.attributes={enabled:{valType:"boolean",dflt:!0,editType:"calc"},groups:{valType:"string",strict:!0,noBlank:!0,arrayOk:!0,dflt:"x",editType:"calc"},aggregations:{_isLinkedToArray:"aggregation",target:{valType:"string",editType:"calc"},func:{valType:"enumerated",values:["count","sum","avg","median","mode","rms","stddev","min","max","first","last","change","range"],dflt:"first",editType:"calc"},funcmode:{valType:"enumerated",values:["sample","population"],dflt:"sample",editType:"calc"},enabled:{valType:"boolean",dflt:!0,editType:"calc"},editType:"calc"},editType:"calc"},c=l.aggregations;function u(t,e,r,a){if(a.enabled){for(var o=a.target,l=i.nestedProperty(e,o),c=l.get(),u=function(t,e){var r=t.func,n=e.d2c,a=e.c2d;switch(r){case"count":return f;case"first":return h;case"last":return p;case"sum":return function(t,e){for(var r=0,i=0;ii&&(i=u,o=c)}}return i?a(o):s};case"rms":return function(t,e){for(var r=0,i=0,o=0;o":return function(t){return h(t)>s};case">=":return function(t){return h(t)>=s};case"[]":return function(t){var e=h(t);return e>=s[0]&&e<=s[1]};case"()":return function(t){var e=h(t);return e>s[0]&&e=s[0]&&es[0]&&e<=s[1]};case"][":return function(t){var e=h(t);return e<=s[0]||e>=s[1]};case")(":return function(t){var e=h(t);return es[1]};case"](":return function(t){var e=h(t);return e<=s[0]||e>s[1]};case")[":return function(t){var e=h(t);return e=s[1]};case"{}":return function(t){return-1!==s.indexOf(h(t))};case"}{":return function(t){return-1===s.indexOf(h(t))}}}(r,a.getDataToCoordFunc(t,e,s,i),h),x={},b={},_=0;d?(g=function(t){x[t.astr]=n.extendDeep([],t.get()),t.set(new Array(f))},v=function(t,e){var r=x[t.astr][e];t.get()[e]=r}):(g=function(t){x[t.astr]=n.extendDeep([],t.get()),t.set([])},v=function(t,e){var r=x[t.astr][e];t.get().push(r)}),k(g);for(var w=o(e.transforms,r),T=0;T1?"%{group} (%{trace})":"%{group}");var l=t.styles,c=o.styles=[];if(l)for(a=0;a0?o-4:o;for(r=0;r>16&255,l[u++]=e>>8&255,l[u++]=255&e;2===s&&(e=i[t.charCodeAt(r)]<<2|i[t.charCodeAt(r+1)]>>4,l[u++]=255&e);1===s&&(e=i[t.charCodeAt(r)]<<10|i[t.charCodeAt(r+1)]<<4|i[t.charCodeAt(r+2)]>>2,l[u++]=e>>8&255,l[u++]=255&e);return l},r.fromByteArray=function(t){for(var e,r=t.length,i=r%3,a=[],o=0,s=r-i;os?s:o+16383));1===i?(e=t[r-1],a.push(n[e>>2]+n[e<<4&63]+"==")):2===i&&(e=(t[r-2]<<8)+t[r-1],a.push(n[e>>10]+n[e>>4&63]+n[e<<2&63]+"="));return a.join("")};for(var n=[],i=[],a="undefined"!=typeof Uint8Array?Uint8Array:Array,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",s=0,l=o.length;s0)throw new Error("Invalid string. Length must be a multiple of 4");var r=t.indexOf("=");return-1===r&&(r=e),[r,r===e?0:4-r%4]}function u(t,e,r){for(var i,a,o=[],s=e;s>18&63]+n[a>>12&63]+n[a>>6&63]+n[63&a]);return o.join("")}i["-".charCodeAt(0)]=62,i["_".charCodeAt(0)]=63},{}],2:[function(t,e,r){},{}],3:[function(t,e,r){(function(e){(function(){ +/*! + * The buffer module from node.js, for the browser. + * + * @author Feross Aboukhadijeh + * @license MIT + */ +"use strict";var e=t("base64-js"),n=t("ieee754");r.Buffer=a,r.SlowBuffer=function(t){+t!=t&&(t=0);return a.alloc(+t)},r.INSPECT_MAX_BYTES=50;function i(t){if(t>2147483647)throw new RangeError('The value "'+t+'" is invalid for option "size"');var e=new Uint8Array(t);return e.__proto__=a.prototype,e}function a(t,e,r){if("number"==typeof t){if("string"==typeof e)throw new TypeError('The "string" argument must be of type string. Received type number');return l(t)}return o(t,e,r)}function o(t,e,r){if("string"==typeof t)return function(t,e){"string"==typeof e&&""!==e||(e="utf8");if(!a.isEncoding(e))throw new TypeError("Unknown encoding: "+e);var r=0|f(t,e),n=i(r),o=n.write(t,e);o!==r&&(n=n.slice(0,o));return n}(t,e);if(ArrayBuffer.isView(t))return c(t);if(null==t)throw TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof t);if(B(t,ArrayBuffer)||t&&B(t.buffer,ArrayBuffer))return function(t,e,r){if(e<0||t.byteLength=2147483647)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+2147483647..toString(16)+" bytes");return 0|t}function f(t,e){if(a.isBuffer(t))return t.length;if(ArrayBuffer.isView(t)||B(t,ArrayBuffer))return t.byteLength;if("string"!=typeof t)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof t);var r=t.length,n=arguments.length>2&&!0===arguments[2];if(!n&&0===r)return 0;for(var i=!1;;)switch(e){case"ascii":case"latin1":case"binary":return r;case"utf8":case"utf-8":return D(t).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*r;case"hex":return r>>>1;case"base64":return R(t).length;default:if(i)return n?-1:D(t).length;e=(""+e).toLowerCase(),i=!0}}function h(t,e,r){var n=!1;if((void 0===e||e<0)&&(e=0),e>this.length)return"";if((void 0===r||r>this.length)&&(r=this.length),r<=0)return"";if((r>>>=0)<=(e>>>=0))return"";for(t||(t="utf8");;)switch(t){case"hex":return M(this,e,r);case"utf8":case"utf-8":return T(this,e,r);case"ascii":return k(this,e,r);case"latin1":case"binary":return A(this,e,r);case"base64":return w(this,e,r);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return S(this,e,r);default:if(n)throw new TypeError("Unknown encoding: "+t);t=(t+"").toLowerCase(),n=!0}}function p(t,e,r){var n=t[e];t[e]=t[r],t[r]=n}function d(t,e,r,n,i){if(0===t.length)return-1;if("string"==typeof r?(n=r,r=0):r>2147483647?r=2147483647:r<-2147483648&&(r=-2147483648),N(r=+r)&&(r=i?0:t.length-1),r<0&&(r=t.length+r),r>=t.length){if(i)return-1;r=t.length-1}else if(r<0){if(!i)return-1;r=0}if("string"==typeof e&&(e=a.from(e,n)),a.isBuffer(e))return 0===e.length?-1:m(t,e,r,n,i);if("number"==typeof e)return e&=255,"function"==typeof Uint8Array.prototype.indexOf?i?Uint8Array.prototype.indexOf.call(t,e,r):Uint8Array.prototype.lastIndexOf.call(t,e,r):m(t,[e],r,n,i);throw new TypeError("val must be string, number or Buffer")}function m(t,e,r,n,i){var a,o=1,s=t.length,l=e.length;if(void 0!==n&&("ucs2"===(n=String(n).toLowerCase())||"ucs-2"===n||"utf16le"===n||"utf-16le"===n)){if(t.length<2||e.length<2)return-1;o=2,s/=2,l/=2,r/=2}function c(t,e){return 1===o?t[e]:t.readUInt16BE(e*o)}if(i){var u=-1;for(a=r;as&&(r=s-l),a=r;a>=0;a--){for(var f=!0,h=0;hi&&(n=i):n=i;var a=e.length;n>a/2&&(n=a/2);for(var o=0;o>8,i=r%256,a.push(i),a.push(n);return a}(e,t.length-r),t,r,n)}function w(t,r,n){return 0===r&&n===t.length?e.fromByteArray(t):e.fromByteArray(t.slice(r,n))}function T(t,e,r){r=Math.min(t.length,r);for(var n=[],i=e;i239?4:c>223?3:c>191?2:1;if(i+f<=r)switch(f){case 1:c<128&&(u=c);break;case 2:128==(192&(a=t[i+1]))&&(l=(31&c)<<6|63&a)>127&&(u=l);break;case 3:a=t[i+1],o=t[i+2],128==(192&a)&&128==(192&o)&&(l=(15&c)<<12|(63&a)<<6|63&o)>2047&&(l<55296||l>57343)&&(u=l);break;case 4:a=t[i+1],o=t[i+2],s=t[i+3],128==(192&a)&&128==(192&o)&&128==(192&s)&&(l=(15&c)<<18|(63&a)<<12|(63&o)<<6|63&s)>65535&&l<1114112&&(u=l)}null===u?(u=65533,f=1):u>65535&&(u-=65536,n.push(u>>>10&1023|55296),u=56320|1023&u),n.push(u),i+=f}return function(t){var e=t.length;if(e<=4096)return String.fromCharCode.apply(String,t);var r="",n=0;for(;ne&&(t+=" ... "),""},a.prototype.compare=function(t,e,r,n,i){if(B(t,Uint8Array)&&(t=a.from(t,t.offset,t.byteLength)),!a.isBuffer(t))throw new TypeError('The "target" argument must be one of type Buffer or Uint8Array. Received type '+typeof t);if(void 0===e&&(e=0),void 0===r&&(r=t?t.length:0),void 0===n&&(n=0),void 0===i&&(i=this.length),e<0||r>t.length||n<0||i>this.length)throw new RangeError("out of range index");if(n>=i&&e>=r)return 0;if(n>=i)return-1;if(e>=r)return 1;if(this===t)return 0;for(var o=(i>>>=0)-(n>>>=0),s=(r>>>=0)-(e>>>=0),l=Math.min(o,s),c=this.slice(n,i),u=t.slice(e,r),f=0;f>>=0,isFinite(r)?(r>>>=0,void 0===n&&(n="utf8")):(n=r,r=void 0)}var i=this.length-e;if((void 0===r||r>i)&&(r=i),t.length>0&&(r<0||e<0)||e>this.length)throw new RangeError("Attempt to write outside buffer bounds");n||(n="utf8");for(var a=!1;;)switch(n){case"hex":return g(this,t,e,r);case"utf8":case"utf-8":return v(this,t,e,r);case"ascii":return y(this,t,e,r);case"latin1":case"binary":return x(this,t,e,r);case"base64":return b(this,t,e,r);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return _(this,t,e,r);default:if(a)throw new TypeError("Unknown encoding: "+n);n=(""+n).toLowerCase(),a=!0}},a.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};function k(t,e,r){var n="";r=Math.min(t.length,r);for(var i=e;in)&&(r=n);for(var i="",a=e;ar)throw new RangeError("Trying to access beyond buffer length")}function L(t,e,r,n,i,o){if(!a.isBuffer(t))throw new TypeError('"buffer" argument must be a Buffer instance');if(e>i||et.length)throw new RangeError("Index out of range")}function C(t,e,r,n,i,a){if(r+n>t.length)throw new RangeError("Index out of range");if(r<0)throw new RangeError("Index out of range")}function P(t,e,r,i,a){return e=+e,r>>>=0,a||C(t,0,r,4),n.write(t,e,r,i,23,4),r+4}function I(t,e,r,i,a){return e=+e,r>>>=0,a||C(t,0,r,8),n.write(t,e,r,i,52,8),r+8}a.prototype.slice=function(t,e){var r=this.length;(t=~~t)<0?(t+=r)<0&&(t=0):t>r&&(t=r),(e=void 0===e?r:~~e)<0?(e+=r)<0&&(e=0):e>r&&(e=r),e>>=0,e>>>=0,r||E(t,e,this.length);for(var n=this[t],i=1,a=0;++a>>=0,e>>>=0,r||E(t,e,this.length);for(var n=this[t+--e],i=1;e>0&&(i*=256);)n+=this[t+--e]*i;return n},a.prototype.readUInt8=function(t,e){return t>>>=0,e||E(t,1,this.length),this[t]},a.prototype.readUInt16LE=function(t,e){return t>>>=0,e||E(t,2,this.length),this[t]|this[t+1]<<8},a.prototype.readUInt16BE=function(t,e){return t>>>=0,e||E(t,2,this.length),this[t]<<8|this[t+1]},a.prototype.readUInt32LE=function(t,e){return t>>>=0,e||E(t,4,this.length),(this[t]|this[t+1]<<8|this[t+2]<<16)+16777216*this[t+3]},a.prototype.readUInt32BE=function(t,e){return t>>>=0,e||E(t,4,this.length),16777216*this[t]+(this[t+1]<<16|this[t+2]<<8|this[t+3])},a.prototype.readIntLE=function(t,e,r){t>>>=0,e>>>=0,r||E(t,e,this.length);for(var n=this[t],i=1,a=0;++a=(i*=128)&&(n-=Math.pow(2,8*e)),n},a.prototype.readIntBE=function(t,e,r){t>>>=0,e>>>=0,r||E(t,e,this.length);for(var n=e,i=1,a=this[t+--n];n>0&&(i*=256);)a+=this[t+--n]*i;return a>=(i*=128)&&(a-=Math.pow(2,8*e)),a},a.prototype.readInt8=function(t,e){return t>>>=0,e||E(t,1,this.length),128&this[t]?-1*(255-this[t]+1):this[t]},a.prototype.readInt16LE=function(t,e){t>>>=0,e||E(t,2,this.length);var r=this[t]|this[t+1]<<8;return 32768&r?4294901760|r:r},a.prototype.readInt16BE=function(t,e){t>>>=0,e||E(t,2,this.length);var r=this[t+1]|this[t]<<8;return 32768&r?4294901760|r:r},a.prototype.readInt32LE=function(t,e){return t>>>=0,e||E(t,4,this.length),this[t]|this[t+1]<<8|this[t+2]<<16|this[t+3]<<24},a.prototype.readInt32BE=function(t,e){return t>>>=0,e||E(t,4,this.length),this[t]<<24|this[t+1]<<16|this[t+2]<<8|this[t+3]},a.prototype.readFloatLE=function(t,e){return t>>>=0,e||E(t,4,this.length),n.read(this,t,!0,23,4)},a.prototype.readFloatBE=function(t,e){return t>>>=0,e||E(t,4,this.length),n.read(this,t,!1,23,4)},a.prototype.readDoubleLE=function(t,e){return t>>>=0,e||E(t,8,this.length),n.read(this,t,!0,52,8)},a.prototype.readDoubleBE=function(t,e){return t>>>=0,e||E(t,8,this.length),n.read(this,t,!1,52,8)},a.prototype.writeUIntLE=function(t,e,r,n){(t=+t,e>>>=0,r>>>=0,n)||L(this,t,e,r,Math.pow(2,8*r)-1,0);var i=1,a=0;for(this[e]=255&t;++a>>=0,r>>>=0,n)||L(this,t,e,r,Math.pow(2,8*r)-1,0);var i=r-1,a=1;for(this[e+i]=255&t;--i>=0&&(a*=256);)this[e+i]=t/a&255;return e+r},a.prototype.writeUInt8=function(t,e,r){return t=+t,e>>>=0,r||L(this,t,e,1,255,0),this[e]=255&t,e+1},a.prototype.writeUInt16LE=function(t,e,r){return t=+t,e>>>=0,r||L(this,t,e,2,65535,0),this[e]=255&t,this[e+1]=t>>>8,e+2},a.prototype.writeUInt16BE=function(t,e,r){return t=+t,e>>>=0,r||L(this,t,e,2,65535,0),this[e]=t>>>8,this[e+1]=255&t,e+2},a.prototype.writeUInt32LE=function(t,e,r){return t=+t,e>>>=0,r||L(this,t,e,4,4294967295,0),this[e+3]=t>>>24,this[e+2]=t>>>16,this[e+1]=t>>>8,this[e]=255&t,e+4},a.prototype.writeUInt32BE=function(t,e,r){return t=+t,e>>>=0,r||L(this,t,e,4,4294967295,0),this[e]=t>>>24,this[e+1]=t>>>16,this[e+2]=t>>>8,this[e+3]=255&t,e+4},a.prototype.writeIntLE=function(t,e,r,n){if(t=+t,e>>>=0,!n){var i=Math.pow(2,8*r-1);L(this,t,e,r,i-1,-i)}var a=0,o=1,s=0;for(this[e]=255&t;++a>0)-s&255;return e+r},a.prototype.writeIntBE=function(t,e,r,n){if(t=+t,e>>>=0,!n){var i=Math.pow(2,8*r-1);L(this,t,e,r,i-1,-i)}var a=r-1,o=1,s=0;for(this[e+a]=255&t;--a>=0&&(o*=256);)t<0&&0===s&&0!==this[e+a+1]&&(s=1),this[e+a]=(t/o>>0)-s&255;return e+r},a.prototype.writeInt8=function(t,e,r){return t=+t,e>>>=0,r||L(this,t,e,1,127,-128),t<0&&(t=255+t+1),this[e]=255&t,e+1},a.prototype.writeInt16LE=function(t,e,r){return t=+t,e>>>=0,r||L(this,t,e,2,32767,-32768),this[e]=255&t,this[e+1]=t>>>8,e+2},a.prototype.writeInt16BE=function(t,e,r){return t=+t,e>>>=0,r||L(this,t,e,2,32767,-32768),this[e]=t>>>8,this[e+1]=255&t,e+2},a.prototype.writeInt32LE=function(t,e,r){return t=+t,e>>>=0,r||L(this,t,e,4,2147483647,-2147483648),this[e]=255&t,this[e+1]=t>>>8,this[e+2]=t>>>16,this[e+3]=t>>>24,e+4},a.prototype.writeInt32BE=function(t,e,r){return t=+t,e>>>=0,r||L(this,t,e,4,2147483647,-2147483648),t<0&&(t=4294967295+t+1),this[e]=t>>>24,this[e+1]=t>>>16,this[e+2]=t>>>8,this[e+3]=255&t,e+4},a.prototype.writeFloatLE=function(t,e,r){return P(this,t,e,!0,r)},a.prototype.writeFloatBE=function(t,e,r){return P(this,t,e,!1,r)},a.prototype.writeDoubleLE=function(t,e,r){return I(this,t,e,!0,r)},a.prototype.writeDoubleBE=function(t,e,r){return I(this,t,e,!1,r)},a.prototype.copy=function(t,e,r,n){if(!a.isBuffer(t))throw new TypeError("argument should be a Buffer");if(r||(r=0),n||0===n||(n=this.length),e>=t.length&&(e=t.length),e||(e=0),n>0&&n=this.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("sourceEnd out of bounds");n>this.length&&(n=this.length),t.length-e=0;--o)t[o+e]=this[o+r];else Uint8Array.prototype.set.call(t,this.subarray(r,n),e);return i},a.prototype.fill=function(t,e,r,n){if("string"==typeof t){if("string"==typeof e?(n=e,e=0,r=this.length):"string"==typeof r&&(n=r,r=this.length),void 0!==n&&"string"!=typeof n)throw new TypeError("encoding must be a string");if("string"==typeof n&&!a.isEncoding(n))throw new TypeError("Unknown encoding: "+n);if(1===t.length){var i=t.charCodeAt(0);("utf8"===n&&i<128||"latin1"===n)&&(t=i)}}else"number"==typeof t&&(t&=255);if(e<0||this.length>>=0,r=void 0===r?this.length:r>>>0,t||(t=0),"number"==typeof t)for(o=e;o55295&&r<57344){if(!i){if(r>56319){(e-=3)>-1&&a.push(239,191,189);continue}if(o+1===n){(e-=3)>-1&&a.push(239,191,189);continue}i=r;continue}if(r<56320){(e-=3)>-1&&a.push(239,191,189),i=r;continue}r=65536+(i-55296<<10|r-56320)}else i&&(e-=3)>-1&&a.push(239,191,189);if(i=null,r<128){if((e-=1)<0)break;a.push(r)}else if(r<2048){if((e-=2)<0)break;a.push(r>>6|192,63&r|128)}else if(r<65536){if((e-=3)<0)break;a.push(r>>12|224,r>>6&63|128,63&r|128)}else{if(!(r<1114112))throw new Error("Invalid code point");if((e-=4)<0)break;a.push(r>>18|240,r>>12&63|128,r>>6&63|128,63&r|128)}}return a}function R(t){return e.toByteArray(function(t){if((t=(t=t.split("=")[0]).trim().replace(O,"")).length<2)return"";for(;t.length%4!=0;)t+="=";return t}(t))}function F(t,e,r,n){for(var i=0;i=e.length||i>=t.length);++i)e[i+r]=t[i];return i}function B(t,e){return t instanceof e||null!=t&&null!=t.constructor&&null!=t.constructor.name&&t.constructor.name===e.name}function N(t){return t!=t}}).call(this)}).call(this,t("buffer").Buffer)},{"base64-js":1,buffer:3,ieee754:4}],4:[function(t,e,r){r.read=function(t,e,r,n,i){var a,o,s=8*i-n-1,l=(1<>1,u=-7,f=r?i-1:0,h=r?-1:1,p=t[e+f];for(f+=h,a=p&(1<<-u)-1,p>>=-u,u+=s;u>0;a=256*a+t[e+f],f+=h,u-=8);for(o=a&(1<<-u)-1,a>>=-u,u+=n;u>0;o=256*o+t[e+f],f+=h,u-=8);if(0===a)a=1-c;else{if(a===l)return o?NaN:1/0*(p?-1:1);o+=Math.pow(2,n),a-=c}return(p?-1:1)*o*Math.pow(2,a-n)},r.write=function(t,e,r,n,i,a){var o,s,l,c=8*a-i-1,u=(1<>1,h=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,p=n?0:a-1,d=n?1:-1,m=e<0||0===e&&1/e<0?1:0;for(e=Math.abs(e),isNaN(e)||e===1/0?(s=isNaN(e)?1:0,o=u):(o=Math.floor(Math.log(e)/Math.LN2),e*(l=Math.pow(2,-o))<1&&(o--,l*=2),(e+=o+f>=1?h/l:h*Math.pow(2,1-f))*l>=2&&(o++,l/=2),o+f>=u?(s=0,o=u):o+f>=1?(s=(e*l-1)*Math.pow(2,i),o+=f):(s=e*Math.pow(2,f-1)*Math.pow(2,i),o=0));i>=8;t[r+p]=255&s,p+=d,s/=256,i-=8);for(o=o<0;t[r+p]=255&o,p+=d,o/=256,c-=8);t[r+p-d]|=128*m}},{}],5:[function(t,e,r){var n,i,a=e.exports={};function o(){throw new Error("setTimeout has not been defined")}function s(){throw new Error("clearTimeout has not been defined")}function l(t){if(n===setTimeout)return setTimeout(t,0);if((n===o||!n)&&setTimeout)return n=setTimeout,setTimeout(t,0);try{return n(t,0)}catch(e){try{return n.call(null,t,0)}catch(e){return n.call(this,t,0)}}}!function(){try{n="function"==typeof setTimeout?setTimeout:o}catch(t){n=o}try{i="function"==typeof clearTimeout?clearTimeout:s}catch(t){i=s}}();var c,u=[],f=!1,h=-1;function p(){f&&c&&(f=!1,c.length?u=c.concat(u):h=-1,u.length&&d())}function d(){if(!f){var t=l(p);f=!0;for(var e=u.length;e;){for(c=u,u=[];++h1)for(var r=1;r0?c=c.ushln(f):f<0&&(u=u.ushln(-f));return s(c,u)}},{"./div":17,"./is-rat":19,"./lib/is-bn":23,"./lib/num-to-bn":24,"./lib/rationalize":25,"./lib/str-to-bn":26}],19:[function(t,e,r){"use strict";var n=t("./lib/is-bn");e.exports=function(t){return Array.isArray(t)&&2===t.length&&n(t[0])&&n(t[1])}},{"./lib/is-bn":23}],20:[function(t,e,r){"use strict";var n=t("bn.js");e.exports=function(t){return t.cmp(new n(0))}},{"bn.js":33}],21:[function(t,e,r){"use strict";var n=t("./bn-sign");e.exports=function(t){var e=t.length,r=t.words,i=0;if(1===e)i=r[0];else if(2===e)i=r[0]+67108864*r[1];else for(var a=0;a20)return 52;return r+32}},{"bit-twiddle":32,"double-bits":64}],23:[function(t,e,r){"use strict";t("bn.js");e.exports=function(t){return t&&"object"==typeof t&&Boolean(t.words)}},{"bn.js":33}],24:[function(t,e,r){"use strict";var n=t("bn.js"),i=t("double-bits");e.exports=function(t){var e=i.exponent(t);return e<52?new n(t):new n(t*Math.pow(2,52-e)).ushln(e-52)}},{"bn.js":33,"double-bits":64}],25:[function(t,e,r){"use strict";var n=t("./num-to-bn"),i=t("./bn-sign");e.exports=function(t,e){var r=i(t),a=i(e);if(0===r)return[n(0),n(1)];if(0===a)return[n(0),n(0)];a<0&&(t=t.neg(),e=e.neg());var o=t.gcd(e);if(o.cmpn(1))return[t.div(o),e.div(o)];return[t,e]}},{"./bn-sign":20,"./num-to-bn":24}],26:[function(t,e,r){"use strict";var n=t("bn.js");e.exports=function(t){return new n(t)}},{"bn.js":33}],27:[function(t,e,r){"use strict";var n=t("./lib/rationalize");e.exports=function(t,e){return n(t[0].mul(e[0]),t[1].mul(e[1]))}},{"./lib/rationalize":25}],28:[function(t,e,r){"use strict";var n=t("./lib/bn-sign");e.exports=function(t){return n(t[0])*n(t[1])}},{"./lib/bn-sign":20}],29:[function(t,e,r){"use strict";var n=t("./lib/rationalize");e.exports=function(t,e){return n(t[0].mul(e[1]).sub(t[1].mul(e[0])),t[1].mul(e[1]))}},{"./lib/rationalize":25}],30:[function(t,e,r){"use strict";var n=t("./lib/bn-to-num"),i=t("./lib/ctz");e.exports=function(t){var e=t[0],r=t[1];if(0===e.cmpn(0))return 0;var a=e.abs().divmod(r.abs()),o=a.div,s=n(o),l=a.mod,c=e.negative!==r.negative?-1:1;if(0===l.cmpn(0))return c*s;if(s){var u=i(s)+4,f=n(l.ushln(u).divRound(r));return c*(s+f*Math.pow(2,-u))}var h=r.bitLength()-l.bitLength()+53;f=n(l.ushln(h).divRound(r));return h<1023?c*f*Math.pow(2,-h):(f*=Math.pow(2,-1023),c*f*Math.pow(2,1023-h))}},{"./lib/bn-to-num":21,"./lib/ctz":22}],31:[function(t,e,r){"use strict";function n(t,e,r,n,i){for(var a=i+1;n<=i;){var o=n+i>>>1,s=t[o];(void 0!==r?r(s,e):s-e)>=0?(a=o,i=o-1):n=o+1}return a}function i(t,e,r,n,i){for(var a=i+1;n<=i;){var o=n+i>>>1,s=t[o];(void 0!==r?r(s,e):s-e)>0?(a=o,i=o-1):n=o+1}return a}function a(t,e,r,n,i){for(var a=n-1;n<=i;){var o=n+i>>>1,s=t[o];(void 0!==r?r(s,e):s-e)<0?(a=o,n=o+1):i=o-1}return a}function o(t,e,r,n,i){for(var a=n-1;n<=i;){var o=n+i>>>1,s=t[o];(void 0!==r?r(s,e):s-e)<=0?(a=o,n=o+1):i=o-1}return a}function s(t,e,r,n,i){for(;n<=i;){var a=n+i>>>1,o=t[a],s=void 0!==r?r(o,e):o-e;if(0===s)return a;s<=0?n=a+1:i=a-1}return-1}function l(t,e,r,n,i,a){return"function"==typeof r?a(t,e,r,void 0===n?0:0|n,void 0===i?t.length-1:0|i):a(t,e,void 0,void 0===r?0:0|r,void 0===n?t.length-1:0|n)}e.exports={ge:function(t,e,r,i,a){return l(t,e,r,i,a,n)},gt:function(t,e,r,n,a){return l(t,e,r,n,a,i)},lt:function(t,e,r,n,i){return l(t,e,r,n,i,a)},le:function(t,e,r,n,i){return l(t,e,r,n,i,o)},eq:function(t,e,r,n,i){return l(t,e,r,n,i,s)}}},{}],32:[function(t,e,r){"use strict";function n(t){var e=32;return(t&=-t)&&e--,65535&t&&(e-=16),16711935&t&&(e-=8),252645135&t&&(e-=4),858993459&t&&(e-=2),1431655765&t&&(e-=1),e}r.INT_BITS=32,r.INT_MAX=2147483647,r.INT_MIN=-1<<31,r.sign=function(t){return(t>0)-(t<0)},r.abs=function(t){var e=t>>31;return(t^e)-e},r.min=function(t,e){return e^(t^e)&-(t65535)<<4,e|=r=((t>>>=e)>255)<<3,e|=r=((t>>>=r)>15)<<2,(e|=r=((t>>>=r)>3)<<1)|(t>>>=r)>>1},r.log10=function(t){return t>=1e9?9:t>=1e8?8:t>=1e7?7:t>=1e6?6:t>=1e5?5:t>=1e4?4:t>=1e3?3:t>=100?2:t>=10?1:0},r.popCount=function(t){return 16843009*((t=(858993459&(t-=t>>>1&1431655765))+(t>>>2&858993459))+(t>>>4)&252645135)>>>24},r.countTrailingZeros=n,r.nextPow2=function(t){return t+=0===t,--t,t|=t>>>1,t|=t>>>2,t|=t>>>4,t|=t>>>8,(t|=t>>>16)+1},r.prevPow2=function(t){return t|=t>>>1,t|=t>>>2,t|=t>>>4,t|=t>>>8,(t|=t>>>16)-(t>>>1)},r.parity=function(t){return t^=t>>>16,t^=t>>>8,t^=t>>>4,27030>>>(t&=15)&1};var i=new Array(256);!function(t){for(var e=0;e<256;++e){var r=e,n=e,i=7;for(r>>>=1;r;r>>>=1)n<<=1,n|=1&r,--i;t[e]=n<>>8&255]<<16|i[t>>>16&255]<<8|i[t>>>24&255]},r.interleave2=function(t,e){return(t=1431655765&((t=858993459&((t=252645135&((t=16711935&((t&=65535)|t<<8))|t<<4))|t<<2))|t<<1))|(e=1431655765&((e=858993459&((e=252645135&((e=16711935&((e&=65535)|e<<8))|e<<4))|e<<2))|e<<1))<<1},r.deinterleave2=function(t,e){return(t=65535&((t=16711935&((t=252645135&((t=858993459&((t=t>>>e&1431655765)|t>>>1))|t>>>2))|t>>>4))|t>>>16))<<16>>16},r.interleave3=function(t,e,r){return t=1227133513&((t=3272356035&((t=251719695&((t=4278190335&((t&=1023)|t<<16))|t<<8))|t<<4))|t<<2),(t|=(e=1227133513&((e=3272356035&((e=251719695&((e=4278190335&((e&=1023)|e<<16))|e<<8))|e<<4))|e<<2))<<1)|(r=1227133513&((r=3272356035&((r=251719695&((r=4278190335&((r&=1023)|r<<16))|r<<8))|r<<4))|r<<2))<<2},r.deinterleave3=function(t,e){return(t=1023&((t=4278190335&((t=251719695&((t=3272356035&((t=t>>>e&1227133513)|t>>>2))|t>>>4))|t>>>8))|t>>>16))<<22>>22},r.nextCombination=function(t){var e=t|t-1;return e+1|(~e&-~e)-1>>>n(t)+1}},{}],33:[function(t,e,r){!function(e,r){"use strict";function n(t,e){if(!t)throw new Error(e||"Assertion failed")}function i(t,e){t.super_=e;var r=function(){};r.prototype=e.prototype,t.prototype=new r,t.prototype.constructor=t}function a(t,e,r){if(a.isBN(t))return t;this.negative=0,this.words=null,this.length=0,this.red=null,null!==t&&("le"!==e&&"be"!==e||(r=e,e=10),this._init(t||0,e||10,r||"be"))}var o;"object"==typeof e?e.exports=a:r.BN=a,a.BN=a,a.wordSize=26;try{o="undefined"!=typeof window&&void 0!==window.Buffer?window.Buffer:t("buffer").Buffer}catch(t){}function s(t,e){var r=t.charCodeAt(e);return r>=65&&r<=70?r-55:r>=97&&r<=102?r-87:r-48&15}function l(t,e,r){var n=s(t,r);return r-1>=e&&(n|=s(t,r-1)<<4),n}function c(t,e,r,n){for(var i=0,a=Math.min(t.length,r),o=e;o=49?s-49+10:s>=17?s-17+10:s}return i}a.isBN=function(t){return t instanceof a||null!==t&&"object"==typeof t&&t.constructor.wordSize===a.wordSize&&Array.isArray(t.words)},a.max=function(t,e){return t.cmp(e)>0?t:e},a.min=function(t,e){return t.cmp(e)<0?t:e},a.prototype._init=function(t,e,r){if("number"==typeof t)return this._initNumber(t,e,r);if("object"==typeof t)return this._initArray(t,e,r);"hex"===e&&(e=16),n(e===(0|e)&&e>=2&&e<=36);var i=0;"-"===(t=t.toString().replace(/\s+/g,""))[0]&&(i++,this.negative=1),i=0;i-=3)o=t[i]|t[i-1]<<8|t[i-2]<<16,this.words[a]|=o<>>26-s&67108863,(s+=24)>=26&&(s-=26,a++);else if("le"===r)for(i=0,a=0;i>>26-s&67108863,(s+=24)>=26&&(s-=26,a++);return this.strip()},a.prototype._parseHex=function(t,e,r){this.length=Math.ceil((t.length-e)/6),this.words=new Array(this.length);for(var n=0;n=e;n-=2)i=l(t,e,n)<=18?(a-=18,o+=1,this.words[o]|=i>>>26):a+=8;else for(n=(t.length-e)%2==0?e+1:e;n=18?(a-=18,o+=1,this.words[o]|=i>>>26):a+=8;this.strip()},a.prototype._parseBase=function(t,e,r){this.words=[0],this.length=1;for(var n=0,i=1;i<=67108863;i*=e)n++;n--,i=i/e|0;for(var a=t.length-r,o=a%n,s=Math.min(a,a-o)+r,l=0,u=r;u1&&0===this.words[this.length-1];)this.length--;return this._normSign()},a.prototype._normSign=function(){return 1===this.length&&0===this.words[0]&&(this.negative=0),this},a.prototype.inspect=function(){return(this.red?""};var u=["","0","00","000","0000","00000","000000","0000000","00000000","000000000","0000000000","00000000000","000000000000","0000000000000","00000000000000","000000000000000","0000000000000000","00000000000000000","000000000000000000","0000000000000000000","00000000000000000000","000000000000000000000","0000000000000000000000","00000000000000000000000","000000000000000000000000","0000000000000000000000000"],f=[0,0,25,16,12,11,10,9,8,8,7,7,7,7,6,6,6,6,6,6,6,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5],h=[0,0,33554432,43046721,16777216,48828125,60466176,40353607,16777216,43046721,1e7,19487171,35831808,62748517,7529536,11390625,16777216,24137569,34012224,47045881,64e6,4084101,5153632,6436343,7962624,9765625,11881376,14348907,17210368,20511149,243e5,28629151,33554432,39135393,45435424,52521875,60466176];function p(t,e,r){r.negative=e.negative^t.negative;var n=t.length+e.length|0;r.length=n,n=n-1|0;var i=0|t.words[0],a=0|e.words[0],o=i*a,s=67108863&o,l=o/67108864|0;r.words[0]=s;for(var c=1;c>>26,f=67108863&l,h=Math.min(c,e.length-1),p=Math.max(0,c-t.length+1);p<=h;p++){var d=c-p|0;u+=(o=(i=0|t.words[d])*(a=0|e.words[p])+f)/67108864|0,f=67108863&o}r.words[c]=0|f,l=0|u}return 0!==l?r.words[c]=0|l:r.length--,r.strip()}a.prototype.toString=function(t,e){var r;if(e=0|e||1,16===(t=t||10)||"hex"===t){r="";for(var i=0,a=0,o=0;o>>24-i&16777215)||o!==this.length-1?u[6-l.length]+l+r:l+r,(i+=2)>=26&&(i-=26,o--)}for(0!==a&&(r=a.toString(16)+r);r.length%e!=0;)r="0"+r;return 0!==this.negative&&(r="-"+r),r}if(t===(0|t)&&t>=2&&t<=36){var c=f[t],p=h[t];r="";var d=this.clone();for(d.negative=0;!d.isZero();){var m=d.modn(p).toString(t);r=(d=d.idivn(p)).isZero()?m+r:u[c-m.length]+m+r}for(this.isZero()&&(r="0"+r);r.length%e!=0;)r="0"+r;return 0!==this.negative&&(r="-"+r),r}n(!1,"Base should be between 2 and 36")},a.prototype.toNumber=function(){var t=this.words[0];return 2===this.length?t+=67108864*this.words[1]:3===this.length&&1===this.words[2]?t+=4503599627370496+67108864*this.words[1]:this.length>2&&n(!1,"Number can only safely store up to 53 bits"),0!==this.negative?-t:t},a.prototype.toJSON=function(){return this.toString(16)},a.prototype.toBuffer=function(t,e){return n(void 0!==o),this.toArrayLike(o,t,e)},a.prototype.toArray=function(t,e){return this.toArrayLike(Array,t,e)},a.prototype.toArrayLike=function(t,e,r){var i=this.byteLength(),a=r||Math.max(1,i);n(i<=a,"byte array longer than desired length"),n(a>0,"Requested array length <= 0"),this.strip();var o,s,l="le"===e,c=new t(a),u=this.clone();if(l){for(s=0;!u.isZero();s++)o=u.andln(255),u.iushrn(8),c[s]=o;for(;s=4096&&(r+=13,e>>>=13),e>=64&&(r+=7,e>>>=7),e>=8&&(r+=4,e>>>=4),e>=2&&(r+=2,e>>>=2),r+e},a.prototype._zeroBits=function(t){if(0===t)return 26;var e=t,r=0;return 0==(8191&e)&&(r+=13,e>>>=13),0==(127&e)&&(r+=7,e>>>=7),0==(15&e)&&(r+=4,e>>>=4),0==(3&e)&&(r+=2,e>>>=2),0==(1&e)&&r++,r},a.prototype.bitLength=function(){var t=this.words[this.length-1],e=this._countBits(t);return 26*(this.length-1)+e},a.prototype.zeroBits=function(){if(this.isZero())return 0;for(var t=0,e=0;et.length?this.clone().ior(t):t.clone().ior(this)},a.prototype.uor=function(t){return this.length>t.length?this.clone().iuor(t):t.clone().iuor(this)},a.prototype.iuand=function(t){var e;e=this.length>t.length?t:this;for(var r=0;rt.length?this.clone().iand(t):t.clone().iand(this)},a.prototype.uand=function(t){return this.length>t.length?this.clone().iuand(t):t.clone().iuand(this)},a.prototype.iuxor=function(t){var e,r;this.length>t.length?(e=this,r=t):(e=t,r=this);for(var n=0;nt.length?this.clone().ixor(t):t.clone().ixor(this)},a.prototype.uxor=function(t){return this.length>t.length?this.clone().iuxor(t):t.clone().iuxor(this)},a.prototype.inotn=function(t){n("number"==typeof t&&t>=0);var e=0|Math.ceil(t/26),r=t%26;this._expand(e),r>0&&e--;for(var i=0;i0&&(this.words[i]=~this.words[i]&67108863>>26-r),this.strip()},a.prototype.notn=function(t){return this.clone().inotn(t)},a.prototype.setn=function(t,e){n("number"==typeof t&&t>=0);var r=t/26|0,i=t%26;return this._expand(r+1),this.words[r]=e?this.words[r]|1<t.length?(r=this,n=t):(r=t,n=this);for(var i=0,a=0;a>>26;for(;0!==i&&a>>26;if(this.length=r.length,0!==i)this.words[this.length]=i,this.length++;else if(r!==this)for(;at.length?this.clone().iadd(t):t.clone().iadd(this)},a.prototype.isub=function(t){if(0!==t.negative){t.negative=0;var e=this.iadd(t);return t.negative=1,e._normSign()}if(0!==this.negative)return this.negative=0,this.iadd(t),this.negative=1,this._normSign();var r,n,i=this.cmp(t);if(0===i)return this.negative=0,this.length=1,this.words[0]=0,this;i>0?(r=this,n=t):(r=t,n=this);for(var a=0,o=0;o>26,this.words[o]=67108863&e;for(;0!==a&&o>26,this.words[o]=67108863&e;if(0===a&&o>>13,p=0|o[1],d=8191&p,m=p>>>13,g=0|o[2],v=8191&g,y=g>>>13,x=0|o[3],b=8191&x,_=x>>>13,w=0|o[4],T=8191&w,k=w>>>13,A=0|o[5],M=8191&A,S=A>>>13,E=0|o[6],L=8191&E,C=E>>>13,P=0|o[7],I=8191&P,O=P>>>13,z=0|o[8],D=8191&z,R=z>>>13,F=0|o[9],B=8191&F,N=F>>>13,j=0|s[0],U=8191&j,V=j>>>13,H=0|s[1],q=8191&H,G=H>>>13,Y=0|s[2],W=8191&Y,X=Y>>>13,Z=0|s[3],J=8191&Z,K=Z>>>13,Q=0|s[4],$=8191&Q,tt=Q>>>13,et=0|s[5],rt=8191&et,nt=et>>>13,it=0|s[6],at=8191&it,ot=it>>>13,st=0|s[7],lt=8191&st,ct=st>>>13,ut=0|s[8],ft=8191&ut,ht=ut>>>13,pt=0|s[9],dt=8191&pt,mt=pt>>>13;r.negative=t.negative^e.negative,r.length=19;var gt=(c+(n=Math.imul(f,U))|0)+((8191&(i=(i=Math.imul(f,V))+Math.imul(h,U)|0))<<13)|0;c=((a=Math.imul(h,V))+(i>>>13)|0)+(gt>>>26)|0,gt&=67108863,n=Math.imul(d,U),i=(i=Math.imul(d,V))+Math.imul(m,U)|0,a=Math.imul(m,V);var vt=(c+(n=n+Math.imul(f,q)|0)|0)+((8191&(i=(i=i+Math.imul(f,G)|0)+Math.imul(h,q)|0))<<13)|0;c=((a=a+Math.imul(h,G)|0)+(i>>>13)|0)+(vt>>>26)|0,vt&=67108863,n=Math.imul(v,U),i=(i=Math.imul(v,V))+Math.imul(y,U)|0,a=Math.imul(y,V),n=n+Math.imul(d,q)|0,i=(i=i+Math.imul(d,G)|0)+Math.imul(m,q)|0,a=a+Math.imul(m,G)|0;var yt=(c+(n=n+Math.imul(f,W)|0)|0)+((8191&(i=(i=i+Math.imul(f,X)|0)+Math.imul(h,W)|0))<<13)|0;c=((a=a+Math.imul(h,X)|0)+(i>>>13)|0)+(yt>>>26)|0,yt&=67108863,n=Math.imul(b,U),i=(i=Math.imul(b,V))+Math.imul(_,U)|0,a=Math.imul(_,V),n=n+Math.imul(v,q)|0,i=(i=i+Math.imul(v,G)|0)+Math.imul(y,q)|0,a=a+Math.imul(y,G)|0,n=n+Math.imul(d,W)|0,i=(i=i+Math.imul(d,X)|0)+Math.imul(m,W)|0,a=a+Math.imul(m,X)|0;var xt=(c+(n=n+Math.imul(f,J)|0)|0)+((8191&(i=(i=i+Math.imul(f,K)|0)+Math.imul(h,J)|0))<<13)|0;c=((a=a+Math.imul(h,K)|0)+(i>>>13)|0)+(xt>>>26)|0,xt&=67108863,n=Math.imul(T,U),i=(i=Math.imul(T,V))+Math.imul(k,U)|0,a=Math.imul(k,V),n=n+Math.imul(b,q)|0,i=(i=i+Math.imul(b,G)|0)+Math.imul(_,q)|0,a=a+Math.imul(_,G)|0,n=n+Math.imul(v,W)|0,i=(i=i+Math.imul(v,X)|0)+Math.imul(y,W)|0,a=a+Math.imul(y,X)|0,n=n+Math.imul(d,J)|0,i=(i=i+Math.imul(d,K)|0)+Math.imul(m,J)|0,a=a+Math.imul(m,K)|0;var bt=(c+(n=n+Math.imul(f,$)|0)|0)+((8191&(i=(i=i+Math.imul(f,tt)|0)+Math.imul(h,$)|0))<<13)|0;c=((a=a+Math.imul(h,tt)|0)+(i>>>13)|0)+(bt>>>26)|0,bt&=67108863,n=Math.imul(M,U),i=(i=Math.imul(M,V))+Math.imul(S,U)|0,a=Math.imul(S,V),n=n+Math.imul(T,q)|0,i=(i=i+Math.imul(T,G)|0)+Math.imul(k,q)|0,a=a+Math.imul(k,G)|0,n=n+Math.imul(b,W)|0,i=(i=i+Math.imul(b,X)|0)+Math.imul(_,W)|0,a=a+Math.imul(_,X)|0,n=n+Math.imul(v,J)|0,i=(i=i+Math.imul(v,K)|0)+Math.imul(y,J)|0,a=a+Math.imul(y,K)|0,n=n+Math.imul(d,$)|0,i=(i=i+Math.imul(d,tt)|0)+Math.imul(m,$)|0,a=a+Math.imul(m,tt)|0;var _t=(c+(n=n+Math.imul(f,rt)|0)|0)+((8191&(i=(i=i+Math.imul(f,nt)|0)+Math.imul(h,rt)|0))<<13)|0;c=((a=a+Math.imul(h,nt)|0)+(i>>>13)|0)+(_t>>>26)|0,_t&=67108863,n=Math.imul(L,U),i=(i=Math.imul(L,V))+Math.imul(C,U)|0,a=Math.imul(C,V),n=n+Math.imul(M,q)|0,i=(i=i+Math.imul(M,G)|0)+Math.imul(S,q)|0,a=a+Math.imul(S,G)|0,n=n+Math.imul(T,W)|0,i=(i=i+Math.imul(T,X)|0)+Math.imul(k,W)|0,a=a+Math.imul(k,X)|0,n=n+Math.imul(b,J)|0,i=(i=i+Math.imul(b,K)|0)+Math.imul(_,J)|0,a=a+Math.imul(_,K)|0,n=n+Math.imul(v,$)|0,i=(i=i+Math.imul(v,tt)|0)+Math.imul(y,$)|0,a=a+Math.imul(y,tt)|0,n=n+Math.imul(d,rt)|0,i=(i=i+Math.imul(d,nt)|0)+Math.imul(m,rt)|0,a=a+Math.imul(m,nt)|0;var wt=(c+(n=n+Math.imul(f,at)|0)|0)+((8191&(i=(i=i+Math.imul(f,ot)|0)+Math.imul(h,at)|0))<<13)|0;c=((a=a+Math.imul(h,ot)|0)+(i>>>13)|0)+(wt>>>26)|0,wt&=67108863,n=Math.imul(I,U),i=(i=Math.imul(I,V))+Math.imul(O,U)|0,a=Math.imul(O,V),n=n+Math.imul(L,q)|0,i=(i=i+Math.imul(L,G)|0)+Math.imul(C,q)|0,a=a+Math.imul(C,G)|0,n=n+Math.imul(M,W)|0,i=(i=i+Math.imul(M,X)|0)+Math.imul(S,W)|0,a=a+Math.imul(S,X)|0,n=n+Math.imul(T,J)|0,i=(i=i+Math.imul(T,K)|0)+Math.imul(k,J)|0,a=a+Math.imul(k,K)|0,n=n+Math.imul(b,$)|0,i=(i=i+Math.imul(b,tt)|0)+Math.imul(_,$)|0,a=a+Math.imul(_,tt)|0,n=n+Math.imul(v,rt)|0,i=(i=i+Math.imul(v,nt)|0)+Math.imul(y,rt)|0,a=a+Math.imul(y,nt)|0,n=n+Math.imul(d,at)|0,i=(i=i+Math.imul(d,ot)|0)+Math.imul(m,at)|0,a=a+Math.imul(m,ot)|0;var Tt=(c+(n=n+Math.imul(f,lt)|0)|0)+((8191&(i=(i=i+Math.imul(f,ct)|0)+Math.imul(h,lt)|0))<<13)|0;c=((a=a+Math.imul(h,ct)|0)+(i>>>13)|0)+(Tt>>>26)|0,Tt&=67108863,n=Math.imul(D,U),i=(i=Math.imul(D,V))+Math.imul(R,U)|0,a=Math.imul(R,V),n=n+Math.imul(I,q)|0,i=(i=i+Math.imul(I,G)|0)+Math.imul(O,q)|0,a=a+Math.imul(O,G)|0,n=n+Math.imul(L,W)|0,i=(i=i+Math.imul(L,X)|0)+Math.imul(C,W)|0,a=a+Math.imul(C,X)|0,n=n+Math.imul(M,J)|0,i=(i=i+Math.imul(M,K)|0)+Math.imul(S,J)|0,a=a+Math.imul(S,K)|0,n=n+Math.imul(T,$)|0,i=(i=i+Math.imul(T,tt)|0)+Math.imul(k,$)|0,a=a+Math.imul(k,tt)|0,n=n+Math.imul(b,rt)|0,i=(i=i+Math.imul(b,nt)|0)+Math.imul(_,rt)|0,a=a+Math.imul(_,nt)|0,n=n+Math.imul(v,at)|0,i=(i=i+Math.imul(v,ot)|0)+Math.imul(y,at)|0,a=a+Math.imul(y,ot)|0,n=n+Math.imul(d,lt)|0,i=(i=i+Math.imul(d,ct)|0)+Math.imul(m,lt)|0,a=a+Math.imul(m,ct)|0;var kt=(c+(n=n+Math.imul(f,ft)|0)|0)+((8191&(i=(i=i+Math.imul(f,ht)|0)+Math.imul(h,ft)|0))<<13)|0;c=((a=a+Math.imul(h,ht)|0)+(i>>>13)|0)+(kt>>>26)|0,kt&=67108863,n=Math.imul(B,U),i=(i=Math.imul(B,V))+Math.imul(N,U)|0,a=Math.imul(N,V),n=n+Math.imul(D,q)|0,i=(i=i+Math.imul(D,G)|0)+Math.imul(R,q)|0,a=a+Math.imul(R,G)|0,n=n+Math.imul(I,W)|0,i=(i=i+Math.imul(I,X)|0)+Math.imul(O,W)|0,a=a+Math.imul(O,X)|0,n=n+Math.imul(L,J)|0,i=(i=i+Math.imul(L,K)|0)+Math.imul(C,J)|0,a=a+Math.imul(C,K)|0,n=n+Math.imul(M,$)|0,i=(i=i+Math.imul(M,tt)|0)+Math.imul(S,$)|0,a=a+Math.imul(S,tt)|0,n=n+Math.imul(T,rt)|0,i=(i=i+Math.imul(T,nt)|0)+Math.imul(k,rt)|0,a=a+Math.imul(k,nt)|0,n=n+Math.imul(b,at)|0,i=(i=i+Math.imul(b,ot)|0)+Math.imul(_,at)|0,a=a+Math.imul(_,ot)|0,n=n+Math.imul(v,lt)|0,i=(i=i+Math.imul(v,ct)|0)+Math.imul(y,lt)|0,a=a+Math.imul(y,ct)|0,n=n+Math.imul(d,ft)|0,i=(i=i+Math.imul(d,ht)|0)+Math.imul(m,ft)|0,a=a+Math.imul(m,ht)|0;var At=(c+(n=n+Math.imul(f,dt)|0)|0)+((8191&(i=(i=i+Math.imul(f,mt)|0)+Math.imul(h,dt)|0))<<13)|0;c=((a=a+Math.imul(h,mt)|0)+(i>>>13)|0)+(At>>>26)|0,At&=67108863,n=Math.imul(B,q),i=(i=Math.imul(B,G))+Math.imul(N,q)|0,a=Math.imul(N,G),n=n+Math.imul(D,W)|0,i=(i=i+Math.imul(D,X)|0)+Math.imul(R,W)|0,a=a+Math.imul(R,X)|0,n=n+Math.imul(I,J)|0,i=(i=i+Math.imul(I,K)|0)+Math.imul(O,J)|0,a=a+Math.imul(O,K)|0,n=n+Math.imul(L,$)|0,i=(i=i+Math.imul(L,tt)|0)+Math.imul(C,$)|0,a=a+Math.imul(C,tt)|0,n=n+Math.imul(M,rt)|0,i=(i=i+Math.imul(M,nt)|0)+Math.imul(S,rt)|0,a=a+Math.imul(S,nt)|0,n=n+Math.imul(T,at)|0,i=(i=i+Math.imul(T,ot)|0)+Math.imul(k,at)|0,a=a+Math.imul(k,ot)|0,n=n+Math.imul(b,lt)|0,i=(i=i+Math.imul(b,ct)|0)+Math.imul(_,lt)|0,a=a+Math.imul(_,ct)|0,n=n+Math.imul(v,ft)|0,i=(i=i+Math.imul(v,ht)|0)+Math.imul(y,ft)|0,a=a+Math.imul(y,ht)|0;var Mt=(c+(n=n+Math.imul(d,dt)|0)|0)+((8191&(i=(i=i+Math.imul(d,mt)|0)+Math.imul(m,dt)|0))<<13)|0;c=((a=a+Math.imul(m,mt)|0)+(i>>>13)|0)+(Mt>>>26)|0,Mt&=67108863,n=Math.imul(B,W),i=(i=Math.imul(B,X))+Math.imul(N,W)|0,a=Math.imul(N,X),n=n+Math.imul(D,J)|0,i=(i=i+Math.imul(D,K)|0)+Math.imul(R,J)|0,a=a+Math.imul(R,K)|0,n=n+Math.imul(I,$)|0,i=(i=i+Math.imul(I,tt)|0)+Math.imul(O,$)|0,a=a+Math.imul(O,tt)|0,n=n+Math.imul(L,rt)|0,i=(i=i+Math.imul(L,nt)|0)+Math.imul(C,rt)|0,a=a+Math.imul(C,nt)|0,n=n+Math.imul(M,at)|0,i=(i=i+Math.imul(M,ot)|0)+Math.imul(S,at)|0,a=a+Math.imul(S,ot)|0,n=n+Math.imul(T,lt)|0,i=(i=i+Math.imul(T,ct)|0)+Math.imul(k,lt)|0,a=a+Math.imul(k,ct)|0,n=n+Math.imul(b,ft)|0,i=(i=i+Math.imul(b,ht)|0)+Math.imul(_,ft)|0,a=a+Math.imul(_,ht)|0;var St=(c+(n=n+Math.imul(v,dt)|0)|0)+((8191&(i=(i=i+Math.imul(v,mt)|0)+Math.imul(y,dt)|0))<<13)|0;c=((a=a+Math.imul(y,mt)|0)+(i>>>13)|0)+(St>>>26)|0,St&=67108863,n=Math.imul(B,J),i=(i=Math.imul(B,K))+Math.imul(N,J)|0,a=Math.imul(N,K),n=n+Math.imul(D,$)|0,i=(i=i+Math.imul(D,tt)|0)+Math.imul(R,$)|0,a=a+Math.imul(R,tt)|0,n=n+Math.imul(I,rt)|0,i=(i=i+Math.imul(I,nt)|0)+Math.imul(O,rt)|0,a=a+Math.imul(O,nt)|0,n=n+Math.imul(L,at)|0,i=(i=i+Math.imul(L,ot)|0)+Math.imul(C,at)|0,a=a+Math.imul(C,ot)|0,n=n+Math.imul(M,lt)|0,i=(i=i+Math.imul(M,ct)|0)+Math.imul(S,lt)|0,a=a+Math.imul(S,ct)|0,n=n+Math.imul(T,ft)|0,i=(i=i+Math.imul(T,ht)|0)+Math.imul(k,ft)|0,a=a+Math.imul(k,ht)|0;var Et=(c+(n=n+Math.imul(b,dt)|0)|0)+((8191&(i=(i=i+Math.imul(b,mt)|0)+Math.imul(_,dt)|0))<<13)|0;c=((a=a+Math.imul(_,mt)|0)+(i>>>13)|0)+(Et>>>26)|0,Et&=67108863,n=Math.imul(B,$),i=(i=Math.imul(B,tt))+Math.imul(N,$)|0,a=Math.imul(N,tt),n=n+Math.imul(D,rt)|0,i=(i=i+Math.imul(D,nt)|0)+Math.imul(R,rt)|0,a=a+Math.imul(R,nt)|0,n=n+Math.imul(I,at)|0,i=(i=i+Math.imul(I,ot)|0)+Math.imul(O,at)|0,a=a+Math.imul(O,ot)|0,n=n+Math.imul(L,lt)|0,i=(i=i+Math.imul(L,ct)|0)+Math.imul(C,lt)|0,a=a+Math.imul(C,ct)|0,n=n+Math.imul(M,ft)|0,i=(i=i+Math.imul(M,ht)|0)+Math.imul(S,ft)|0,a=a+Math.imul(S,ht)|0;var Lt=(c+(n=n+Math.imul(T,dt)|0)|0)+((8191&(i=(i=i+Math.imul(T,mt)|0)+Math.imul(k,dt)|0))<<13)|0;c=((a=a+Math.imul(k,mt)|0)+(i>>>13)|0)+(Lt>>>26)|0,Lt&=67108863,n=Math.imul(B,rt),i=(i=Math.imul(B,nt))+Math.imul(N,rt)|0,a=Math.imul(N,nt),n=n+Math.imul(D,at)|0,i=(i=i+Math.imul(D,ot)|0)+Math.imul(R,at)|0,a=a+Math.imul(R,ot)|0,n=n+Math.imul(I,lt)|0,i=(i=i+Math.imul(I,ct)|0)+Math.imul(O,lt)|0,a=a+Math.imul(O,ct)|0,n=n+Math.imul(L,ft)|0,i=(i=i+Math.imul(L,ht)|0)+Math.imul(C,ft)|0,a=a+Math.imul(C,ht)|0;var Ct=(c+(n=n+Math.imul(M,dt)|0)|0)+((8191&(i=(i=i+Math.imul(M,mt)|0)+Math.imul(S,dt)|0))<<13)|0;c=((a=a+Math.imul(S,mt)|0)+(i>>>13)|0)+(Ct>>>26)|0,Ct&=67108863,n=Math.imul(B,at),i=(i=Math.imul(B,ot))+Math.imul(N,at)|0,a=Math.imul(N,ot),n=n+Math.imul(D,lt)|0,i=(i=i+Math.imul(D,ct)|0)+Math.imul(R,lt)|0,a=a+Math.imul(R,ct)|0,n=n+Math.imul(I,ft)|0,i=(i=i+Math.imul(I,ht)|0)+Math.imul(O,ft)|0,a=a+Math.imul(O,ht)|0;var Pt=(c+(n=n+Math.imul(L,dt)|0)|0)+((8191&(i=(i=i+Math.imul(L,mt)|0)+Math.imul(C,dt)|0))<<13)|0;c=((a=a+Math.imul(C,mt)|0)+(i>>>13)|0)+(Pt>>>26)|0,Pt&=67108863,n=Math.imul(B,lt),i=(i=Math.imul(B,ct))+Math.imul(N,lt)|0,a=Math.imul(N,ct),n=n+Math.imul(D,ft)|0,i=(i=i+Math.imul(D,ht)|0)+Math.imul(R,ft)|0,a=a+Math.imul(R,ht)|0;var It=(c+(n=n+Math.imul(I,dt)|0)|0)+((8191&(i=(i=i+Math.imul(I,mt)|0)+Math.imul(O,dt)|0))<<13)|0;c=((a=a+Math.imul(O,mt)|0)+(i>>>13)|0)+(It>>>26)|0,It&=67108863,n=Math.imul(B,ft),i=(i=Math.imul(B,ht))+Math.imul(N,ft)|0,a=Math.imul(N,ht);var Ot=(c+(n=n+Math.imul(D,dt)|0)|0)+((8191&(i=(i=i+Math.imul(D,mt)|0)+Math.imul(R,dt)|0))<<13)|0;c=((a=a+Math.imul(R,mt)|0)+(i>>>13)|0)+(Ot>>>26)|0,Ot&=67108863;var zt=(c+(n=Math.imul(B,dt))|0)+((8191&(i=(i=Math.imul(B,mt))+Math.imul(N,dt)|0))<<13)|0;return c=((a=Math.imul(N,mt))+(i>>>13)|0)+(zt>>>26)|0,zt&=67108863,l[0]=gt,l[1]=vt,l[2]=yt,l[3]=xt,l[4]=bt,l[5]=_t,l[6]=wt,l[7]=Tt,l[8]=kt,l[9]=At,l[10]=Mt,l[11]=St,l[12]=Et,l[13]=Lt,l[14]=Ct,l[15]=Pt,l[16]=It,l[17]=Ot,l[18]=zt,0!==c&&(l[19]=c,r.length++),r};function m(t,e,r){return(new g).mulp(t,e,r)}function g(t,e){this.x=t,this.y=e}Math.imul||(d=p),a.prototype.mulTo=function(t,e){var r=this.length+t.length;return 10===this.length&&10===t.length?d(this,t,e):r<63?p(this,t,e):r<1024?function(t,e,r){r.negative=e.negative^t.negative,r.length=t.length+e.length;for(var n=0,i=0,a=0;a>>26)|0)>>>26,o&=67108863}r.words[a]=s,n=o,o=i}return 0!==n?r.words[a]=n:r.length--,r.strip()}(this,t,e):m(this,t,e)},g.prototype.makeRBT=function(t){for(var e=new Array(t),r=a.prototype._countBits(t)-1,n=0;n>=1;return n},g.prototype.permute=function(t,e,r,n,i,a){for(var o=0;o>>=1)i++;return 1<>>=13,r[2*o+1]=8191&a,a>>>=13;for(o=2*e;o>=26,e+=i/67108864|0,e+=a>>>26,this.words[r]=67108863&a}return 0!==e&&(this.words[r]=e,this.length++),this},a.prototype.muln=function(t){return this.clone().imuln(t)},a.prototype.sqr=function(){return this.mul(this)},a.prototype.isqr=function(){return this.imul(this.clone())},a.prototype.pow=function(t){var e=function(t){for(var e=new Array(t.bitLength()),r=0;r>>i}return e}(t);if(0===e.length)return new a(1);for(var r=this,n=0;n=0);var e,r=t%26,i=(t-r)/26,a=67108863>>>26-r<<26-r;if(0!==r){var o=0;for(e=0;e>>26-r}o&&(this.words[e]=o,this.length++)}if(0!==i){for(e=this.length-1;e>=0;e--)this.words[e+i]=this.words[e];for(e=0;e=0),i=e?(e-e%26)/26:0;var a=t%26,o=Math.min((t-a)/26,this.length),s=67108863^67108863>>>a<o)for(this.length-=o,c=0;c=0&&(0!==u||c>=i);c--){var f=0|this.words[c];this.words[c]=u<<26-a|f>>>a,u=f&s}return l&&0!==u&&(l.words[l.length++]=u),0===this.length&&(this.words[0]=0,this.length=1),this.strip()},a.prototype.ishrn=function(t,e,r){return n(0===this.negative),this.iushrn(t,e,r)},a.prototype.shln=function(t){return this.clone().ishln(t)},a.prototype.ushln=function(t){return this.clone().iushln(t)},a.prototype.shrn=function(t){return this.clone().ishrn(t)},a.prototype.ushrn=function(t){return this.clone().iushrn(t)},a.prototype.testn=function(t){n("number"==typeof t&&t>=0);var e=t%26,r=(t-e)/26,i=1<=0);var e=t%26,r=(t-e)/26;if(n(0===this.negative,"imaskn works only with positive numbers"),this.length<=r)return this;if(0!==e&&r++,this.length=Math.min(r,this.length),0!==e){var i=67108863^67108863>>>e<=67108864;e++)this.words[e]-=67108864,e===this.length-1?this.words[e+1]=1:this.words[e+1]++;return this.length=Math.max(this.length,e+1),this},a.prototype.isubn=function(t){if(n("number"==typeof t),n(t<67108864),t<0)return this.iaddn(-t);if(0!==this.negative)return this.negative=0,this.iaddn(t),this.negative=1,this;if(this.words[0]-=t,1===this.length&&this.words[0]<0)this.words[0]=-this.words[0],this.negative=1;else for(var e=0;e>26)-(l/67108864|0),this.words[i+r]=67108863&a}for(;i>26,this.words[i+r]=67108863&a;if(0===s)return this.strip();for(n(-1===s),s=0,i=0;i>26,this.words[i]=67108863&a;return this.negative=1,this.strip()},a.prototype._wordDiv=function(t,e){var r=(this.length,t.length),n=this.clone(),i=t,o=0|i.words[i.length-1];0!==(r=26-this._countBits(o))&&(i=i.ushln(r),n.iushln(r),o=0|i.words[i.length-1]);var s,l=n.length-i.length;if("mod"!==e){(s=new a(null)).length=l+1,s.words=new Array(s.length);for(var c=0;c=0;f--){var h=67108864*(0|n.words[i.length+f])+(0|n.words[i.length+f-1]);for(h=Math.min(h/o|0,67108863),n._ishlnsubmul(i,h,f);0!==n.negative;)h--,n.negative=0,n._ishlnsubmul(i,1,f),n.isZero()||(n.negative^=1);s&&(s.words[f]=h)}return s&&s.strip(),n.strip(),"div"!==e&&0!==r&&n.iushrn(r),{div:s||null,mod:n}},a.prototype.divmod=function(t,e,r){return n(!t.isZero()),this.isZero()?{div:new a(0),mod:new a(0)}:0!==this.negative&&0===t.negative?(s=this.neg().divmod(t,e),"mod"!==e&&(i=s.div.neg()),"div"!==e&&(o=s.mod.neg(),r&&0!==o.negative&&o.iadd(t)),{div:i,mod:o}):0===this.negative&&0!==t.negative?(s=this.divmod(t.neg(),e),"mod"!==e&&(i=s.div.neg()),{div:i,mod:s.mod}):0!=(this.negative&t.negative)?(s=this.neg().divmod(t.neg(),e),"div"!==e&&(o=s.mod.neg(),r&&0!==o.negative&&o.isub(t)),{div:s.div,mod:o}):t.length>this.length||this.cmp(t)<0?{div:new a(0),mod:this}:1===t.length?"div"===e?{div:this.divn(t.words[0]),mod:null}:"mod"===e?{div:null,mod:new a(this.modn(t.words[0]))}:{div:this.divn(t.words[0]),mod:new a(this.modn(t.words[0]))}:this._wordDiv(t,e);var i,o,s},a.prototype.div=function(t){return this.divmod(t,"div",!1).div},a.prototype.mod=function(t){return this.divmod(t,"mod",!1).mod},a.prototype.umod=function(t){return this.divmod(t,"mod",!0).mod},a.prototype.divRound=function(t){var e=this.divmod(t);if(e.mod.isZero())return e.div;var r=0!==e.div.negative?e.mod.isub(t):e.mod,n=t.ushrn(1),i=t.andln(1),a=r.cmp(n);return a<0||1===i&&0===a?e.div:0!==e.div.negative?e.div.isubn(1):e.div.iaddn(1)},a.prototype.modn=function(t){n(t<=67108863);for(var e=(1<<26)%t,r=0,i=this.length-1;i>=0;i--)r=(e*r+(0|this.words[i]))%t;return r},a.prototype.idivn=function(t){n(t<=67108863);for(var e=0,r=this.length-1;r>=0;r--){var i=(0|this.words[r])+67108864*e;this.words[r]=i/t|0,e=i%t}return this.strip()},a.prototype.divn=function(t){return this.clone().idivn(t)},a.prototype.egcd=function(t){n(0===t.negative),n(!t.isZero());var e=this,r=t.clone();e=0!==e.negative?e.umod(t):e.clone();for(var i=new a(1),o=new a(0),s=new a(0),l=new a(1),c=0;e.isEven()&&r.isEven();)e.iushrn(1),r.iushrn(1),++c;for(var u=r.clone(),f=e.clone();!e.isZero();){for(var h=0,p=1;0==(e.words[0]&p)&&h<26;++h,p<<=1);if(h>0)for(e.iushrn(h);h-- >0;)(i.isOdd()||o.isOdd())&&(i.iadd(u),o.isub(f)),i.iushrn(1),o.iushrn(1);for(var d=0,m=1;0==(r.words[0]&m)&&d<26;++d,m<<=1);if(d>0)for(r.iushrn(d);d-- >0;)(s.isOdd()||l.isOdd())&&(s.iadd(u),l.isub(f)),s.iushrn(1),l.iushrn(1);e.cmp(r)>=0?(e.isub(r),i.isub(s),o.isub(l)):(r.isub(e),s.isub(i),l.isub(o))}return{a:s,b:l,gcd:r.iushln(c)}},a.prototype._invmp=function(t){n(0===t.negative),n(!t.isZero());var e=this,r=t.clone();e=0!==e.negative?e.umod(t):e.clone();for(var i,o=new a(1),s=new a(0),l=r.clone();e.cmpn(1)>0&&r.cmpn(1)>0;){for(var c=0,u=1;0==(e.words[0]&u)&&c<26;++c,u<<=1);if(c>0)for(e.iushrn(c);c-- >0;)o.isOdd()&&o.iadd(l),o.iushrn(1);for(var f=0,h=1;0==(r.words[0]&h)&&f<26;++f,h<<=1);if(f>0)for(r.iushrn(f);f-- >0;)s.isOdd()&&s.iadd(l),s.iushrn(1);e.cmp(r)>=0?(e.isub(r),o.isub(s)):(r.isub(e),s.isub(o))}return(i=0===e.cmpn(1)?o:s).cmpn(0)<0&&i.iadd(t),i},a.prototype.gcd=function(t){if(this.isZero())return t.abs();if(t.isZero())return this.abs();var e=this.clone(),r=t.clone();e.negative=0,r.negative=0;for(var n=0;e.isEven()&&r.isEven();n++)e.iushrn(1),r.iushrn(1);for(;;){for(;e.isEven();)e.iushrn(1);for(;r.isEven();)r.iushrn(1);var i=e.cmp(r);if(i<0){var a=e;e=r,r=a}else if(0===i||0===r.cmpn(1))break;e.isub(r)}return r.iushln(n)},a.prototype.invm=function(t){return this.egcd(t).a.umod(t)},a.prototype.isEven=function(){return 0==(1&this.words[0])},a.prototype.isOdd=function(){return 1==(1&this.words[0])},a.prototype.andln=function(t){return this.words[0]&t},a.prototype.bincn=function(t){n("number"==typeof t);var e=t%26,r=(t-e)/26,i=1<>>26,s&=67108863,this.words[o]=s}return 0!==a&&(this.words[o]=a,this.length++),this},a.prototype.isZero=function(){return 1===this.length&&0===this.words[0]},a.prototype.cmpn=function(t){var e,r=t<0;if(0!==this.negative&&!r)return-1;if(0===this.negative&&r)return 1;if(this.strip(),this.length>1)e=1;else{r&&(t=-t),n(t<=67108863,"Number is too big");var i=0|this.words[0];e=i===t?0:it.length)return 1;if(this.length=0;r--){var n=0|this.words[r],i=0|t.words[r];if(n!==i){ni&&(e=1);break}}return e},a.prototype.gtn=function(t){return 1===this.cmpn(t)},a.prototype.gt=function(t){return 1===this.cmp(t)},a.prototype.gten=function(t){return this.cmpn(t)>=0},a.prototype.gte=function(t){return this.cmp(t)>=0},a.prototype.ltn=function(t){return-1===this.cmpn(t)},a.prototype.lt=function(t){return-1===this.cmp(t)},a.prototype.lten=function(t){return this.cmpn(t)<=0},a.prototype.lte=function(t){return this.cmp(t)<=0},a.prototype.eqn=function(t){return 0===this.cmpn(t)},a.prototype.eq=function(t){return 0===this.cmp(t)},a.red=function(t){return new T(t)},a.prototype.toRed=function(t){return n(!this.red,"Already a number in reduction context"),n(0===this.negative,"red works only with positives"),t.convertTo(this)._forceRed(t)},a.prototype.fromRed=function(){return n(this.red,"fromRed works only with numbers in reduction context"),this.red.convertFrom(this)},a.prototype._forceRed=function(t){return this.red=t,this},a.prototype.forceRed=function(t){return n(!this.red,"Already a number in reduction context"),this._forceRed(t)},a.prototype.redAdd=function(t){return n(this.red,"redAdd works only with red numbers"),this.red.add(this,t)},a.prototype.redIAdd=function(t){return n(this.red,"redIAdd works only with red numbers"),this.red.iadd(this,t)},a.prototype.redSub=function(t){return n(this.red,"redSub works only with red numbers"),this.red.sub(this,t)},a.prototype.redISub=function(t){return n(this.red,"redISub works only with red numbers"),this.red.isub(this,t)},a.prototype.redShl=function(t){return n(this.red,"redShl works only with red numbers"),this.red.shl(this,t)},a.prototype.redMul=function(t){return n(this.red,"redMul works only with red numbers"),this.red._verify2(this,t),this.red.mul(this,t)},a.prototype.redIMul=function(t){return n(this.red,"redMul works only with red numbers"),this.red._verify2(this,t),this.red.imul(this,t)},a.prototype.redSqr=function(){return n(this.red,"redSqr works only with red numbers"),this.red._verify1(this),this.red.sqr(this)},a.prototype.redISqr=function(){return n(this.red,"redISqr works only with red numbers"),this.red._verify1(this),this.red.isqr(this)},a.prototype.redSqrt=function(){return n(this.red,"redSqrt works only with red numbers"),this.red._verify1(this),this.red.sqrt(this)},a.prototype.redInvm=function(){return n(this.red,"redInvm works only with red numbers"),this.red._verify1(this),this.red.invm(this)},a.prototype.redNeg=function(){return n(this.red,"redNeg works only with red numbers"),this.red._verify1(this),this.red.neg(this)},a.prototype.redPow=function(t){return n(this.red&&!t.red,"redPow(normalNum)"),this.red._verify1(this),this.red.pow(this,t)};var v={k256:null,p224:null,p192:null,p25519:null};function y(t,e){this.name=t,this.p=new a(e,16),this.n=this.p.bitLength(),this.k=new a(1).iushln(this.n).isub(this.p),this.tmp=this._tmp()}function x(){y.call(this,"k256","ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffe fffffc2f")}function b(){y.call(this,"p224","ffffffff ffffffff ffffffff ffffffff 00000000 00000000 00000001")}function _(){y.call(this,"p192","ffffffff ffffffff ffffffff fffffffe ffffffff ffffffff")}function w(){y.call(this,"25519","7fffffffffffffff ffffffffffffffff ffffffffffffffff ffffffffffffffed")}function T(t){if("string"==typeof t){var e=a._prime(t);this.m=e.p,this.prime=e}else n(t.gtn(1),"modulus must be greater than 1"),this.m=t,this.prime=null}function k(t){T.call(this,t),this.shift=this.m.bitLength(),this.shift%26!=0&&(this.shift+=26-this.shift%26),this.r=new a(1).iushln(this.shift),this.r2=this.imod(this.r.sqr()),this.rinv=this.r._invmp(this.m),this.minv=this.rinv.mul(this.r).isubn(1).div(this.m),this.minv=this.minv.umod(this.r),this.minv=this.r.sub(this.minv)}y.prototype._tmp=function(){var t=new a(null);return t.words=new Array(Math.ceil(this.n/13)),t},y.prototype.ireduce=function(t){var e,r=t;do{this.split(r,this.tmp),e=(r=(r=this.imulK(r)).iadd(this.tmp)).bitLength()}while(e>this.n);var n=e0?r.isub(this.p):void 0!==r.strip?r.strip():r._strip(),r},y.prototype.split=function(t,e){t.iushrn(this.n,0,e)},y.prototype.imulK=function(t){return t.imul(this.k)},i(x,y),x.prototype.split=function(t,e){for(var r=Math.min(t.length,9),n=0;n>>22,i=a}i>>>=22,t.words[n-10]=i,0===i&&t.length>10?t.length-=10:t.length-=9},x.prototype.imulK=function(t){t.words[t.length]=0,t.words[t.length+1]=0,t.length+=2;for(var e=0,r=0;r>>=26,t.words[r]=i,e=n}return 0!==e&&(t.words[t.length++]=e),t},a._prime=function(t){if(v[t])return v[t];var e;if("k256"===t)e=new x;else if("p224"===t)e=new b;else if("p192"===t)e=new _;else{if("p25519"!==t)throw new Error("Unknown prime "+t);e=new w}return v[t]=e,e},T.prototype._verify1=function(t){n(0===t.negative,"red works only with positives"),n(t.red,"red works only with red numbers")},T.prototype._verify2=function(t,e){n(0==(t.negative|e.negative),"red works only with positives"),n(t.red&&t.red===e.red,"red works only with red numbers")},T.prototype.imod=function(t){return this.prime?this.prime.ireduce(t)._forceRed(this):t.umod(this.m)._forceRed(this)},T.prototype.neg=function(t){return t.isZero()?t.clone():this.m.sub(t)._forceRed(this)},T.prototype.add=function(t,e){this._verify2(t,e);var r=t.add(e);return r.cmp(this.m)>=0&&r.isub(this.m),r._forceRed(this)},T.prototype.iadd=function(t,e){this._verify2(t,e);var r=t.iadd(e);return r.cmp(this.m)>=0&&r.isub(this.m),r},T.prototype.sub=function(t,e){this._verify2(t,e);var r=t.sub(e);return r.cmpn(0)<0&&r.iadd(this.m),r._forceRed(this)},T.prototype.isub=function(t,e){this._verify2(t,e);var r=t.isub(e);return r.cmpn(0)<0&&r.iadd(this.m),r},T.prototype.shl=function(t,e){return this._verify1(t),this.imod(t.ushln(e))},T.prototype.imul=function(t,e){return this._verify2(t,e),this.imod(t.imul(e))},T.prototype.mul=function(t,e){return this._verify2(t,e),this.imod(t.mul(e))},T.prototype.isqr=function(t){return this.imul(t,t.clone())},T.prototype.sqr=function(t){return this.mul(t,t)},T.prototype.sqrt=function(t){if(t.isZero())return t.clone();var e=this.m.andln(3);if(n(e%2==1),3===e){var r=this.m.add(new a(1)).iushrn(2);return this.pow(t,r)}for(var i=this.m.subn(1),o=0;!i.isZero()&&0===i.andln(1);)o++,i.iushrn(1);n(!i.isZero());var s=new a(1).toRed(this),l=s.redNeg(),c=this.m.subn(1).iushrn(1),u=this.m.bitLength();for(u=new a(2*u*u).toRed(this);0!==this.pow(u,c).cmp(l);)u.redIAdd(l);for(var f=this.pow(u,i),h=this.pow(t,i.addn(1).iushrn(1)),p=this.pow(t,i),d=o;0!==p.cmp(s);){for(var m=p,g=0;0!==m.cmp(s);g++)m=m.redSqr();n(g=0;n--){for(var c=e.words[n],u=l-1;u>=0;u--){var f=c>>u&1;i!==r[0]&&(i=this.sqr(i)),0!==f||0!==o?(o<<=1,o|=f,(4===++s||0===n&&0===u)&&(i=this.mul(i,r[o]),s=0,o=0)):s=0}l=26}return i},T.prototype.convertTo=function(t){var e=t.umod(this.m);return e===t?e.clone():e},T.prototype.convertFrom=function(t){var e=t.clone();return e.red=null,e},a.mont=function(t){return new k(t)},i(k,T),k.prototype.convertTo=function(t){return this.imod(t.ushln(this.shift))},k.prototype.convertFrom=function(t){var e=this.imod(t.mul(this.rinv));return e.red=null,e},k.prototype.imul=function(t,e){if(t.isZero()||e.isZero())return t.words[0]=0,t.length=1,t;var r=t.imul(e),n=r.maskn(this.shift).mul(this.minv).imaskn(this.shift).mul(this.m),i=r.isub(n).iushrn(this.shift),a=i;return i.cmp(this.m)>=0?a=i.isub(this.m):i.cmpn(0)<0&&(a=i.iadd(this.m)),a._forceRed(this)},k.prototype.mul=function(t,e){if(t.isZero()||e.isZero())return new a(0)._forceRed(this);var r=t.mul(e),n=r.maskn(this.shift).mul(this.minv).imaskn(this.shift).mul(this.m),i=r.isub(n).iushrn(this.shift),o=i;return i.cmp(this.m)>=0?o=i.isub(this.m):i.cmpn(0)<0&&(o=i.iadd(this.m)),o._forceRed(this)},k.prototype.invm=function(t){return this.imod(t._invmp(this.m).mul(this.r2))._forceRed(this)}}(void 0===e||e,this)},{buffer:2}],34:[function(t,e,r){"use strict";e.exports=function(t){var e,r,n,i=t.length,a=0;for(e=0;e>>1;if(!(u<=0)){var f,h=i.mallocDouble(2*u*s),p=i.mallocInt32(s);if((s=l(t,u,h,p))>0){if(1===u&&n)a.init(s),f=a.sweepComplete(u,r,0,s,h,p,0,s,h,p);else{var d=i.mallocDouble(2*u*c),m=i.mallocInt32(c);(c=l(e,u,d,m))>0&&(a.init(s+c),f=1===u?a.sweepBipartite(u,r,0,s,h,p,0,c,d,m):o(u,r,n,s,h,p,c,d,m),i.free(d),i.free(m))}i.free(h),i.free(p)}return f}}}function u(t,e){n.push([t,e])}function f(t){return n=[],c(t,t,u,!0),n}function h(t,e){return n=[],c(t,e,u,!1),n}},{"./lib/intersect":37,"./lib/sweep":41,"typedarray-pool":308}],36:[function(t,e,r){"use strict";function n(t){return t?function(t,e,r,n,i,a,o,s,l,c,u){return i-n>l-s?function(t,e,r,n,i,a,o,s,l,c,u){for(var f=2*t,h=n,p=f*n;hc-l?n?function(t,e,r,n,i,a,o,s,l,c,u){for(var f=2*t,h=n,p=f*n;h0;){var L=6*(S-=1),C=v[L],P=v[L+1],I=v[L+2],O=v[L+3],z=v[L+4],D=v[L+5],R=2*S,F=y[R],B=y[R+1],N=1&D,j=!!(16&D),U=u,V=w,H=k,q=A;if(N&&(U=k,V=A,H=u,q=w),!(2&D&&(I=p(t,C,P,I,U,V,B),P>=I)||4&D&&(P=d(t,C,P,I,U,V,F))>=I)){var G=I-P,Y=z-O;if(j){if(t*G*(G+Y)<1<<22){if(void 0!==(M=l.scanComplete(t,C,e,P,I,U,V,O,z,H,q)))return M;continue}}else{if(t*Math.min(G,Y)<128){if(void 0!==(M=o(t,C,e,N,P,I,U,V,O,z,H,q)))return M;continue}if(t*G*Y<1<<22){if(void 0!==(M=l.scanBipartite(t,C,e,N,P,I,U,V,O,z,H,q)))return M;continue}}var W=f(t,C,P,I,U,V,F,B);if(P=p0)&&!(p1>=hi)"),h=u("lo===p0"),p=u("lo>>1,f=2*t,h=u,p=o[f*u+e];for(;l=y?(h=v,p=y):g>=b?(h=m,p=g):(h=x,p=b):y>=b?(h=v,p=y):b>=g?(h=m,p=g):(h=x,p=b);for(var _=f*(c-1),w=f*h,T=0;Tr&&i[f+e]>c;--u,f-=o){for(var h=f,p=f+o,d=0;dh;++h,l+=s){if(i[l+f]===o)if(u===h)u+=1,c+=s;else{for(var p=0;s>p;++p){var d=i[l+p];i[l+p]=i[c],i[c++]=d}var m=a[h];a[h]=a[u],a[u++]=m}}return u},"loh;++h,l+=s){if(i[l+f]p;++p){var d=i[l+p];i[l+p]=i[c],i[c++]=d}var m=a[h];a[h]=a[u],a[u++]=m}}return u},"lo<=p0":function(t,e,r,n,i,a,o){for(var s=2*t,l=s*r,c=l,u=r,f=t+e,h=r;n>h;++h,l+=s){if(i[l+f]<=o)if(u===h)u+=1,c+=s;else{for(var p=0;s>p;++p){var d=i[l+p];i[l+p]=i[c],i[c++]=d}var m=a[h];a[h]=a[u],a[u++]=m}}return u},"hi<=p0":function(t,e,r,n,i,a,o){for(var s=2*t,l=s*r,c=l,u=r,f=t+e,h=r;n>h;++h,l+=s){if(i[l+f]<=o)if(u===h)u+=1,c+=s;else{for(var p=0;s>p;++p){var d=i[l+p];i[l+p]=i[c],i[c++]=d}var m=a[h];a[h]=a[u],a[u++]=m}}return u},"lop;++p,l+=s){var d=i[l+f],m=i[l+h];if(dg;++g){var v=i[l+g];i[l+g]=i[c],i[c++]=v}var y=a[p];a[p]=a[u],a[u++]=y}}return u},"lo<=p0&&p0<=hi":function(t,e,r,n,i,a,o){for(var s=2*t,l=s*r,c=l,u=r,f=e,h=t+e,p=r;n>p;++p,l+=s){var d=i[l+f],m=i[l+h];if(d<=o&&o<=m)if(u===p)u+=1,c+=s;else{for(var g=0;s>g;++g){var v=i[l+g];i[l+g]=i[c],i[c++]=v}var y=a[p];a[p]=a[u],a[u++]=y}}return u},"!(lo>=p0)&&!(p1>=hi)":function(t,e,r,n,i,a,o,s){for(var l=2*t,c=l*r,u=c,f=r,h=e,p=t+e,d=r;n>d;++d,c+=l){var m=i[c+h],g=i[c+p];if(!(m>=o||s>=g))if(f===d)f+=1,u+=l;else{for(var v=0;l>v;++v){var y=i[c+v];i[c+v]=i[u],i[u++]=y}var x=a[d];a[d]=a[f],a[f++]=x}}return f}}},{}],40:[function(t,e,r){"use strict";e.exports=function(t,e){e<=128?n(0,e-1,t):function t(e,r,u){var f=(r-e+1)/6|0,h=e+f,p=r-f,d=e+r>>1,m=d-f,g=d+f,v=h,y=m,x=d,b=g,_=p,w=e+1,T=r-1,k=0;l(v,y,u)&&(k=v,v=y,y=k);l(b,_,u)&&(k=b,b=_,_=k);l(v,x,u)&&(k=v,v=x,x=k);l(y,x,u)&&(k=y,y=x,x=k);l(v,b,u)&&(k=v,v=b,b=k);l(x,b,u)&&(k=x,x=b,b=k);l(y,_,u)&&(k=y,y=_,_=k);l(y,x,u)&&(k=y,y=x,x=k);l(b,_,u)&&(k=b,b=_,_=k);for(var A=u[2*y],M=u[2*y+1],S=u[2*b],E=u[2*b+1],L=2*v,C=2*x,P=2*_,I=2*h,O=2*d,z=2*p,D=0;D<2;++D){var R=u[L+D],F=u[C+D],B=u[P+D];u[I+D]=R,u[O+D]=F,u[z+D]=B}a(m,e,u),a(g,r,u);for(var N=w;N<=T;++N)if(c(N,A,M,u))N!==w&&i(N,w,u),++w;else if(!c(N,S,E,u))for(;;){if(c(T,S,E,u)){c(T,A,M,u)?(o(N,w,T,u),++w,--T):(i(N,T,u),--T);break}if(--Tt;){var c=r[l-2],u=r[l-1];if(cr[e+1])}function c(t,e,r,n){var i=n[t*=2];return i>>1;a(h,M);var S=0,E=0;for(w=0;w=1<<28)p(l,c,E--,L=L-(1<<28)|0);else if(L>=0)p(o,s,S--,L);else if(L<=-(1<<28)){L=-L-(1<<28)|0;for(var C=0;C>>1;a(h,E);var L=0,C=0,P=0;for(k=0;k>1==h[2*k+3]>>1&&(O=2,k+=1),I<0){for(var z=-(I>>1)-1,D=0;D>1)-1;0===O?p(o,s,L--,z):1===O?p(l,c,C--,z):2===O&&p(u,f,P--,z)}}},scanBipartite:function(t,e,r,n,i,l,c,u,f,m,g,v){var y=0,x=2*t,b=e,_=e+t,w=1,T=1;n?T=1<<28:w=1<<28;for(var k=i;k>>1;a(h,E);var L=0;for(k=0;k=1<<28?(P=!n,A-=1<<28):(P=!!n,A-=1),P)d(o,s,L++,A);else{var I=v[A],O=x*A,z=g[O+e+1],D=g[O+e+1+t];t:for(var R=0;R>>1;a(h,w);var T=0;for(y=0;y=1<<28)o[T++]=x-(1<<28);else{var A=p[x-=1],M=m*x,S=f[M+e+1],E=f[M+e+1+t];t:for(var L=0;L=0;--L)if(o[L]===x){for(O=L+1;O0;){for(var p=r.pop(),d=(s=r.pop(),u=-1,f=-1,l=o[s],1);d=0||(e.flip(s,p),i(t,e,r,u,s,f),i(t,e,r,s,f,u),i(t,e,r,f,p,u),i(t,e,r,p,u,f)))}}},{"binary-search-bounds":31,"robust-in-sphere":282}],44:[function(t,e,r){"use strict";var n,i=t("binary-search-bounds");function a(t,e,r,n,i,a,o){this.cells=t,this.neighbor=e,this.flags=n,this.constraint=r,this.active=i,this.next=a,this.boundary=o}function o(t,e){return t[0]-e[0]||t[1]-e[1]||t[2]-e[2]}e.exports=function(t,e,r){var n=function(t,e){for(var r=t.cells(),n=r.length,i=0;i0||l.length>0;){for(;s.length>0;){var p=s.pop();if(c[p]!==-i){c[p]=i;u[p];for(var d=0;d<3;++d){var m=h[3*p+d];m>=0&&0===c[m]&&(f[3*p+d]?l.push(m):(s.push(m),c[m]=i))}}}var g=l;l=s,s=g,l.length=0,i=-i}var v=function(t,e,r){for(var n=0,i=0;i1&&i(r[h[p-2]],r[h[p-1]],a)>0;)t.push([h[p-1],h[p-2],o]),p-=1;h.length=p,h.push(o);var d=f.upperIds;for(p=d.length;p>1&&i(r[d[p-2]],r[d[p-1]],a)<0;)t.push([d[p-2],d[p-1],o]),p-=1;d.length=p,d.push(o)}}function u(t,e){var r;return(r=t.a[0]d[0]&&i.push(new o(d,p,2,l),new o(p,d,1,l))}i.sort(s);for(var m=i[0].a[0]-(1+Math.abs(i[0].a[0]))*Math.pow(2,-52),g=[new a([m,1],[m,0],-1,[],[],[],[])],v=[],y=(l=0,i.length);l=0}}(),a.removeTriangle=function(t,e,r){var n=this.stars;o(n[t],e,r),o(n[e],r,t),o(n[r],t,e)},a.addTriangle=function(t,e,r){var n=this.stars;n[t].push(e,r),n[e].push(r,t),n[r].push(t,e)},a.opposite=function(t,e){for(var r=this.stars[e],n=1,i=r.length;ne[2]?1:0)}function v(t,e,r){if(0!==t.length){if(e)for(var n=0;n=0;--a){var x=e[u=(S=n[a])[0]],b=x[0],_=x[1],w=t[b],T=t[_];if((w[0]-T[0]||w[1]-T[1])<0){var k=b;b=_,_=k}x[0]=b;var A,M=x[1]=S[1];for(i&&(A=x[2]);a>0&&n[a-1][0]===u;){var S,E=(S=n[--a])[1];i?e.push([M,E,A]):e.push([M,E]),M=E}i?e.push([M,_,A]):e.push([M,_])}return h}(t,e,h,g,r));return v(e,y,r),!!y||(h.length>0||g.length>0)}},{"./lib/rat-seg-intersect":51,"big-rat":18,"big-rat/cmp":16,"big-rat/to-float":30,"box-intersect":35,nextafter:260,"rat-vec":273,"robust-segment-intersect":287,"union-find":309}],51:[function(t,e,r){"use strict";e.exports=function(t,e,r,n){var a=s(e,t),f=s(n,r),h=u(a,f);if(0===o(h))return null;var p=s(t,r),d=u(f,p),m=i(d,h),g=c(a,m);return l(t,g)};var n=t("big-rat/mul"),i=t("big-rat/div"),a=t("big-rat/sub"),o=t("big-rat/sign"),s=t("rat-vec/sub"),l=t("rat-vec/add"),c=t("rat-vec/muls");function u(t,e){return a(n(t[0],e[1]),n(t[1],e[0]))}},{"big-rat/div":17,"big-rat/mul":27,"big-rat/sign":28,"big-rat/sub":29,"rat-vec/add":272,"rat-vec/muls":274,"rat-vec/sub":275}],52:[function(t,e,r){e.exports={jet:[{index:0,rgb:[0,0,131]},{index:.125,rgb:[0,60,170]},{index:.375,rgb:[5,255,255]},{index:.625,rgb:[255,255,0]},{index:.875,rgb:[250,0,0]},{index:1,rgb:[128,0,0]}],hsv:[{index:0,rgb:[255,0,0]},{index:.169,rgb:[253,255,2]},{index:.173,rgb:[247,255,2]},{index:.337,rgb:[0,252,4]},{index:.341,rgb:[0,252,10]},{index:.506,rgb:[1,249,255]},{index:.671,rgb:[2,0,253]},{index:.675,rgb:[8,0,253]},{index:.839,rgb:[255,0,251]},{index:.843,rgb:[255,0,245]},{index:1,rgb:[255,0,6]}],hot:[{index:0,rgb:[0,0,0]},{index:.3,rgb:[230,0,0]},{index:.6,rgb:[255,210,0]},{index:1,rgb:[255,255,255]}],spring:[{index:0,rgb:[255,0,255]},{index:1,rgb:[255,255,0]}],summer:[{index:0,rgb:[0,128,102]},{index:1,rgb:[255,255,102]}],autumn:[{index:0,rgb:[255,0,0]},{index:1,rgb:[255,255,0]}],winter:[{index:0,rgb:[0,0,255]},{index:1,rgb:[0,255,128]}],bone:[{index:0,rgb:[0,0,0]},{index:.376,rgb:[84,84,116]},{index:.753,rgb:[169,200,200]},{index:1,rgb:[255,255,255]}],copper:[{index:0,rgb:[0,0,0]},{index:.804,rgb:[255,160,102]},{index:1,rgb:[255,199,127]}],greys:[{index:0,rgb:[0,0,0]},{index:1,rgb:[255,255,255]}],yignbu:[{index:0,rgb:[8,29,88]},{index:.125,rgb:[37,52,148]},{index:.25,rgb:[34,94,168]},{index:.375,rgb:[29,145,192]},{index:.5,rgb:[65,182,196]},{index:.625,rgb:[127,205,187]},{index:.75,rgb:[199,233,180]},{index:.875,rgb:[237,248,217]},{index:1,rgb:[255,255,217]}],greens:[{index:0,rgb:[0,68,27]},{index:.125,rgb:[0,109,44]},{index:.25,rgb:[35,139,69]},{index:.375,rgb:[65,171,93]},{index:.5,rgb:[116,196,118]},{index:.625,rgb:[161,217,155]},{index:.75,rgb:[199,233,192]},{index:.875,rgb:[229,245,224]},{index:1,rgb:[247,252,245]}],yiorrd:[{index:0,rgb:[128,0,38]},{index:.125,rgb:[189,0,38]},{index:.25,rgb:[227,26,28]},{index:.375,rgb:[252,78,42]},{index:.5,rgb:[253,141,60]},{index:.625,rgb:[254,178,76]},{index:.75,rgb:[254,217,118]},{index:.875,rgb:[255,237,160]},{index:1,rgb:[255,255,204]}],bluered:[{index:0,rgb:[0,0,255]},{index:1,rgb:[255,0,0]}],rdbu:[{index:0,rgb:[5,10,172]},{index:.35,rgb:[106,137,247]},{index:.5,rgb:[190,190,190]},{index:.6,rgb:[220,170,132]},{index:.7,rgb:[230,145,90]},{index:1,rgb:[178,10,28]}],picnic:[{index:0,rgb:[0,0,255]},{index:.1,rgb:[51,153,255]},{index:.2,rgb:[102,204,255]},{index:.3,rgb:[153,204,255]},{index:.4,rgb:[204,204,255]},{index:.5,rgb:[255,255,255]},{index:.6,rgb:[255,204,255]},{index:.7,rgb:[255,153,255]},{index:.8,rgb:[255,102,204]},{index:.9,rgb:[255,102,102]},{index:1,rgb:[255,0,0]}],rainbow:[{index:0,rgb:[150,0,90]},{index:.125,rgb:[0,0,200]},{index:.25,rgb:[0,25,255]},{index:.375,rgb:[0,152,255]},{index:.5,rgb:[44,255,150]},{index:.625,rgb:[151,255,0]},{index:.75,rgb:[255,234,0]},{index:.875,rgb:[255,111,0]},{index:1,rgb:[255,0,0]}],portland:[{index:0,rgb:[12,51,131]},{index:.25,rgb:[10,136,186]},{index:.5,rgb:[242,211,56]},{index:.75,rgb:[242,143,56]},{index:1,rgb:[217,30,30]}],blackbody:[{index:0,rgb:[0,0,0]},{index:.2,rgb:[230,0,0]},{index:.4,rgb:[230,210,0]},{index:.7,rgb:[255,255,255]},{index:1,rgb:[160,200,255]}],earth:[{index:0,rgb:[0,0,130]},{index:.1,rgb:[0,180,180]},{index:.2,rgb:[40,210,40]},{index:.4,rgb:[230,230,50]},{index:.6,rgb:[120,70,20]},{index:1,rgb:[255,255,255]}],electric:[{index:0,rgb:[0,0,0]},{index:.15,rgb:[30,0,100]},{index:.4,rgb:[120,0,100]},{index:.6,rgb:[160,90,0]},{index:.8,rgb:[230,200,0]},{index:1,rgb:[255,250,220]}],alpha:[{index:0,rgb:[255,255,255,0]},{index:1,rgb:[255,255,255,1]}],viridis:[{index:0,rgb:[68,1,84]},{index:.13,rgb:[71,44,122]},{index:.25,rgb:[59,81,139]},{index:.38,rgb:[44,113,142]},{index:.5,rgb:[33,144,141]},{index:.63,rgb:[39,173,129]},{index:.75,rgb:[92,200,99]},{index:.88,rgb:[170,220,50]},{index:1,rgb:[253,231,37]}],inferno:[{index:0,rgb:[0,0,4]},{index:.13,rgb:[31,12,72]},{index:.25,rgb:[85,15,109]},{index:.38,rgb:[136,34,106]},{index:.5,rgb:[186,54,85]},{index:.63,rgb:[227,89,51]},{index:.75,rgb:[249,140,10]},{index:.88,rgb:[249,201,50]},{index:1,rgb:[252,255,164]}],magma:[{index:0,rgb:[0,0,4]},{index:.13,rgb:[28,16,68]},{index:.25,rgb:[79,18,123]},{index:.38,rgb:[129,37,129]},{index:.5,rgb:[181,54,122]},{index:.63,rgb:[229,80,100]},{index:.75,rgb:[251,135,97]},{index:.88,rgb:[254,194,135]},{index:1,rgb:[252,253,191]}],plasma:[{index:0,rgb:[13,8,135]},{index:.13,rgb:[75,3,161]},{index:.25,rgb:[125,3,168]},{index:.38,rgb:[168,34,150]},{index:.5,rgb:[203,70,121]},{index:.63,rgb:[229,107,93]},{index:.75,rgb:[248,148,65]},{index:.88,rgb:[253,195,40]},{index:1,rgb:[240,249,33]}],warm:[{index:0,rgb:[125,0,179]},{index:.13,rgb:[172,0,187]},{index:.25,rgb:[219,0,170]},{index:.38,rgb:[255,0,130]},{index:.5,rgb:[255,63,74]},{index:.63,rgb:[255,123,0]},{index:.75,rgb:[234,176,0]},{index:.88,rgb:[190,228,0]},{index:1,rgb:[147,255,0]}],cool:[{index:0,rgb:[125,0,179]},{index:.13,rgb:[116,0,218]},{index:.25,rgb:[98,74,237]},{index:.38,rgb:[68,146,231]},{index:.5,rgb:[0,204,197]},{index:.63,rgb:[0,247,146]},{index:.75,rgb:[0,255,88]},{index:.88,rgb:[40,255,8]},{index:1,rgb:[147,255,0]}],"rainbow-soft":[{index:0,rgb:[125,0,179]},{index:.1,rgb:[199,0,180]},{index:.2,rgb:[255,0,121]},{index:.3,rgb:[255,108,0]},{index:.4,rgb:[222,194,0]},{index:.5,rgb:[150,255,0]},{index:.6,rgb:[0,255,55]},{index:.7,rgb:[0,246,150]},{index:.8,rgb:[50,167,222]},{index:.9,rgb:[103,51,235]},{index:1,rgb:[124,0,186]}],bathymetry:[{index:0,rgb:[40,26,44]},{index:.13,rgb:[59,49,90]},{index:.25,rgb:[64,76,139]},{index:.38,rgb:[63,110,151]},{index:.5,rgb:[72,142,158]},{index:.63,rgb:[85,174,163]},{index:.75,rgb:[120,206,163]},{index:.88,rgb:[187,230,172]},{index:1,rgb:[253,254,204]}],cdom:[{index:0,rgb:[47,15,62]},{index:.13,rgb:[87,23,86]},{index:.25,rgb:[130,28,99]},{index:.38,rgb:[171,41,96]},{index:.5,rgb:[206,67,86]},{index:.63,rgb:[230,106,84]},{index:.75,rgb:[242,149,103]},{index:.88,rgb:[249,193,135]},{index:1,rgb:[254,237,176]}],chlorophyll:[{index:0,rgb:[18,36,20]},{index:.13,rgb:[25,63,41]},{index:.25,rgb:[24,91,59]},{index:.38,rgb:[13,119,72]},{index:.5,rgb:[18,148,80]},{index:.63,rgb:[80,173,89]},{index:.75,rgb:[132,196,122]},{index:.88,rgb:[175,221,162]},{index:1,rgb:[215,249,208]}],density:[{index:0,rgb:[54,14,36]},{index:.13,rgb:[89,23,80]},{index:.25,rgb:[110,45,132]},{index:.38,rgb:[120,77,178]},{index:.5,rgb:[120,113,213]},{index:.63,rgb:[115,151,228]},{index:.75,rgb:[134,185,227]},{index:.88,rgb:[177,214,227]},{index:1,rgb:[230,241,241]}],"freesurface-blue":[{index:0,rgb:[30,4,110]},{index:.13,rgb:[47,14,176]},{index:.25,rgb:[41,45,236]},{index:.38,rgb:[25,99,212]},{index:.5,rgb:[68,131,200]},{index:.63,rgb:[114,156,197]},{index:.75,rgb:[157,181,203]},{index:.88,rgb:[200,208,216]},{index:1,rgb:[241,237,236]}],"freesurface-red":[{index:0,rgb:[60,9,18]},{index:.13,rgb:[100,17,27]},{index:.25,rgb:[142,20,29]},{index:.38,rgb:[177,43,27]},{index:.5,rgb:[192,87,63]},{index:.63,rgb:[205,125,105]},{index:.75,rgb:[216,162,148]},{index:.88,rgb:[227,199,193]},{index:1,rgb:[241,237,236]}],oxygen:[{index:0,rgb:[64,5,5]},{index:.13,rgb:[106,6,15]},{index:.25,rgb:[144,26,7]},{index:.38,rgb:[168,64,3]},{index:.5,rgb:[188,100,4]},{index:.63,rgb:[206,136,11]},{index:.75,rgb:[220,174,25]},{index:.88,rgb:[231,215,44]},{index:1,rgb:[248,254,105]}],par:[{index:0,rgb:[51,20,24]},{index:.13,rgb:[90,32,35]},{index:.25,rgb:[129,44,34]},{index:.38,rgb:[159,68,25]},{index:.5,rgb:[182,99,19]},{index:.63,rgb:[199,134,22]},{index:.75,rgb:[212,171,35]},{index:.88,rgb:[221,210,54]},{index:1,rgb:[225,253,75]}],phase:[{index:0,rgb:[145,105,18]},{index:.13,rgb:[184,71,38]},{index:.25,rgb:[186,58,115]},{index:.38,rgb:[160,71,185]},{index:.5,rgb:[110,97,218]},{index:.63,rgb:[50,123,164]},{index:.75,rgb:[31,131,110]},{index:.88,rgb:[77,129,34]},{index:1,rgb:[145,105,18]}],salinity:[{index:0,rgb:[42,24,108]},{index:.13,rgb:[33,50,162]},{index:.25,rgb:[15,90,145]},{index:.38,rgb:[40,118,137]},{index:.5,rgb:[59,146,135]},{index:.63,rgb:[79,175,126]},{index:.75,rgb:[120,203,104]},{index:.88,rgb:[193,221,100]},{index:1,rgb:[253,239,154]}],temperature:[{index:0,rgb:[4,35,51]},{index:.13,rgb:[23,51,122]},{index:.25,rgb:[85,59,157]},{index:.38,rgb:[129,79,143]},{index:.5,rgb:[175,95,130]},{index:.63,rgb:[222,112,101]},{index:.75,rgb:[249,146,66]},{index:.88,rgb:[249,196,65]},{index:1,rgb:[232,250,91]}],turbidity:[{index:0,rgb:[34,31,27]},{index:.13,rgb:[65,50,41]},{index:.25,rgb:[98,69,52]},{index:.38,rgb:[131,89,57]},{index:.5,rgb:[161,112,59]},{index:.63,rgb:[185,140,66]},{index:.75,rgb:[202,174,88]},{index:.88,rgb:[216,209,126]},{index:1,rgb:[233,246,171]}],"velocity-blue":[{index:0,rgb:[17,32,64]},{index:.13,rgb:[35,52,116]},{index:.25,rgb:[29,81,156]},{index:.38,rgb:[31,113,162]},{index:.5,rgb:[50,144,169]},{index:.63,rgb:[87,173,176]},{index:.75,rgb:[149,196,189]},{index:.88,rgb:[203,221,211]},{index:1,rgb:[254,251,230]}],"velocity-green":[{index:0,rgb:[23,35,19]},{index:.13,rgb:[24,64,38]},{index:.25,rgb:[11,95,45]},{index:.38,rgb:[39,123,35]},{index:.5,rgb:[95,146,12]},{index:.63,rgb:[152,165,18]},{index:.75,rgb:[201,186,69]},{index:.88,rgb:[233,216,137]},{index:1,rgb:[255,253,205]}],cubehelix:[{index:0,rgb:[0,0,0]},{index:.07,rgb:[22,5,59]},{index:.13,rgb:[60,4,105]},{index:.2,rgb:[109,1,135]},{index:.27,rgb:[161,0,147]},{index:.33,rgb:[210,2,142]},{index:.4,rgb:[251,11,123]},{index:.47,rgb:[255,29,97]},{index:.53,rgb:[255,54,69]},{index:.6,rgb:[255,85,46]},{index:.67,rgb:[255,120,34]},{index:.73,rgb:[255,157,37]},{index:.8,rgb:[241,191,57]},{index:.87,rgb:[224,220,93]},{index:.93,rgb:[218,241,142]},{index:1,rgb:[227,253,198]}]}},{}],53:[function(t,e,r){"use strict";var n=t("./colorScale"),i=t("lerp");function a(t){return[t[0]/255,t[1]/255,t[2]/255,t[3]]}function o(t){for(var e,r="#",n=0;n<3;++n)r+=("00"+(e=(e=t[n]).toString(16))).substr(e.length);return r}function s(t){return"rgba("+t.join(",")+")"}e.exports=function(t){var e,r,l,c,u,f,h,p,d,m;t||(t={});p=(t.nshades||72)-1,h=t.format||"hex",(f=t.colormap)||(f="jet");if("string"==typeof f){if(f=f.toLowerCase(),!n[f])throw Error(f+" not a supported colorscale");u=n[f]}else{if(!Array.isArray(f))throw Error("unsupported colormap option",f);u=f.slice()}if(u.length>p+1)throw new Error(f+" map requires nshades to be at least size "+u.length);d=Array.isArray(t.alpha)?2!==t.alpha.length?[1,1]:t.alpha.slice():"number"==typeof t.alpha?[t.alpha,t.alpha]:[1,1];e=u.map((function(t){return Math.round(t.index*p)})),d[0]=Math.min(Math.max(d[0],0),1),d[1]=Math.min(Math.max(d[1],0),1);var g=u.map((function(t,e){var r=u[e].index,n=u[e].rgb.slice();return 4===n.length&&n[3]>=0&&n[3]<=1||(n[3]=d[0]+(d[1]-d[0])*r),n})),v=[];for(m=0;m0||l(t,e,a)?-1:1:0===s?c>0||l(t,e,r)?1:-1:i(c-s)}var h=n(t,e,r);return h>0?o>0&&n(t,e,a)>0?1:-1:h<0?o>0||n(t,e,a)>0?1:-1:n(t,e,a)>0||l(t,e,r)?1:-1};var n=t("robust-orientation"),i=t("signum"),a=t("two-sum"),o=t("robust-product"),s=t("robust-sum");function l(t,e,r){var n=a(t[0],-e[0]),i=a(t[1],-e[1]),l=a(r[0],-e[0]),c=a(r[1],-e[1]),u=s(o(n,l),o(i,c));return u[u.length-1]>=0}},{"robust-orientation":284,"robust-product":285,"robust-sum":289,signum:55,"two-sum":307}],55:[function(t,e,r){"use strict";e.exports=function(t){return t<0?-1:t>0?1:0}},{}],56:[function(t,e,r){e.exports=function(t,e){var r=t.length,a=t.length-e.length;if(a)return a;switch(r){case 0:return 0;case 1:return t[0]-e[0];case 2:return t[0]+t[1]-e[0]-e[1]||n(t[0],t[1])-n(e[0],e[1]);case 3:var o=t[0]+t[1],s=e[0]+e[1];if(a=o+t[2]-(s+e[2]))return a;var l=n(t[0],t[1]),c=n(e[0],e[1]);return n(l,t[2])-n(c,e[2])||n(l+t[2],o)-n(c+e[2],s);case 4:var u=t[0],f=t[1],h=t[2],p=t[3],d=e[0],m=e[1],g=e[2],v=e[3];return u+f+h+p-(d+m+g+v)||n(u,f,h,p)-n(d,m,g,v,d)||n(u+f,u+h,u+p,f+h,f+p,h+p)-n(d+m,d+g,d+v,m+g,m+v,g+v)||n(u+f+h,u+f+p,u+h+p,f+h+p)-n(d+m+g,d+m+v,d+g+v,m+g+v);default:for(var y=t.slice().sort(i),x=e.slice().sort(i),b=0;bt[r][0]&&(r=n);return er?[[r],[e]]:[[e]]}},{}],60:[function(t,e,r){"use strict";e.exports=function(t){var e=n(t),r=e.length;if(r<=2)return[];for(var i=new Array(r),a=e[r-1],o=0;o=e[l]&&(s+=1);a[o]=s}}return t}(n(a,!0),r)}};var n=t("incremental-convex-hull"),i=t("affine-hull")},{"affine-hull":10,"incremental-convex-hull":233}],62:[function(t,e,r){"use strict";e.exports=function(t,e,r,n,i,a){var o=i-1,s=i*i,l=o*o,c=(1+2*i)*l,u=i*l,f=s*(3-2*i),h=s*o;if(t.length){a||(a=new Array(t.length));for(var p=t.length-1;p>=0;--p)a[p]=c*t[p]+u*e[p]+f*r[p]+h*n[p];return a}return c*t+u*e+f*r+h*n},e.exports.derivative=function(t,e,r,n,i,a){var o=6*i*i-6*i,s=3*i*i-4*i+1,l=-6*i*i+6*i,c=3*i*i-2*i;if(t.length){a||(a=new Array(t.length));for(var u=t.length-1;u>=0;--u)a[u]=o*t[u]+s*e[u]+l*r[u]+c*n[u];return a}return o*t+s*e+l*r[u]+c*n}},{}],63:[function(t,e,r){"use strict";var n=t("incremental-convex-hull"),i=t("uniq");function a(t,e){this.point=t,this.index=e}function o(t,e){for(var r=t.point,n=e.point,i=r.length,a=0;a=2)return!1;t[r]=n}return!0})):_.filter((function(t){for(var e=0;e<=s;++e){var r=v[t[e]];if(r<0)return!1;t[e]=r}return!0}));if(1&s)for(u=0;u<_.length;++u){h=(b=_[u])[0];b[0]=b[1],b[1]=h}return _}},{"incremental-convex-hull":233,uniq:310}],64:[function(t,e,r){(function(t){(function(){var r=!1;if("undefined"!=typeof Float64Array){var n=new Float64Array(1),i=new Uint32Array(n.buffer);if(n[0]=1,r=!0,1072693248===i[1]){e.exports=function(t){return n[0]=t,[i[0],i[1]]},e.exports.pack=function(t,e){return i[0]=t,i[1]=e,n[0]},e.exports.lo=function(t){return n[0]=t,i[0]},e.exports.hi=function(t){return n[0]=t,i[1]}}else if(1072693248===i[0]){e.exports=function(t){return n[0]=t,[i[1],i[0]]},e.exports.pack=function(t,e){return i[1]=t,i[0]=e,n[0]},e.exports.lo=function(t){return n[0]=t,i[1]},e.exports.hi=function(t){return n[0]=t,i[0]}}else r=!1}if(!r){var a=new t(8);e.exports=function(t){return a.writeDoubleLE(t,0,!0),[a.readUInt32LE(0,!0),a.readUInt32LE(4,!0)]},e.exports.pack=function(t,e){return a.writeUInt32LE(t,0,!0),a.writeUInt32LE(e,4,!0),a.readDoubleLE(0,!0)},e.exports.lo=function(t){return a.writeDoubleLE(t,0,!0),a.readUInt32LE(0,!0)},e.exports.hi=function(t){return a.writeDoubleLE(t,0,!0),a.readUInt32LE(4,!0)}}e.exports.sign=function(t){return e.exports.hi(t)>>>31},e.exports.exponent=function(t){return(e.exports.hi(t)<<1>>>21)-1023},e.exports.fraction=function(t){var r=e.exports.lo(t),n=e.exports.hi(t),i=1048575&n;return 2146435072&n&&(i+=1<<20),[r,i]},e.exports.denormalized=function(t){return!(2146435072&e.exports.hi(t))}}).call(this)}).call(this,t("buffer").Buffer)},{buffer:3}],65:[function(t,e,r){"use strict";e.exports=function(t,e){switch(void 0===e&&(e=0),typeof t){case"number":if(t>0)return function(t,e){var r,n;for(r=new Array(t),n=0;n=r-1){h=l.length-1;var d=t-e[r-1];for(p=0;p=r-1)for(var u=s.length-1,f=(e[r-1],0);f=0;--r)if(t[--e])return!1;return!0},s.jump=function(t){var e=this.lastT(),r=this.dimension;if(!(t0;--f)n.push(a(l[f-1],c[f-1],arguments[f])),i.push(0)}},s.push=function(t){var e=this.lastT(),r=this.dimension;if(!(t1e-6?1/s:0;this._time.push(t);for(var h=r;h>0;--h){var p=a(c[h-1],u[h-1],arguments[h]);n.push(p),i.push((p-n[o++])*f)}}},s.set=function(t){var e=this.dimension;if(!(t0;--l)r.push(a(o[l-1],s[l-1],arguments[l])),n.push(0)}},s.move=function(t){var e=this.lastT(),r=this.dimension;if(!(t<=e||arguments.length!==r+1)){var n=this._state,i=this._velocity,o=n.length-this.dimension,s=this.bounds,l=s[0],c=s[1],u=t-e,f=u>1e-6?1/u:0;this._time.push(t);for(var h=r;h>0;--h){var p=arguments[h];n.push(a(l[h-1],c[h-1],n[o++]+p)),i.push(p*f)}}},s.idle=function(t){var e=this.lastT();if(!(t=0;--f)n.push(a(l[f],c[f],n[o]+u*i[o])),i.push(0),o+=1}}},{"binary-search-bounds":31,"cubic-hermite":62}],69:[function(t,e,r){"use strict";e.exports=function(t){return new s(t||m,null)};function n(t,e,r,n,i,a){this._color=t,this.key=e,this.value=r,this.left=n,this.right=i,this._count=a}function i(t){return new n(t._color,t.key,t.value,t.left,t.right,t._count)}function a(t,e){return new n(t,e.key,e.value,e.left,e.right,e._count)}function o(t){t._count=1+(t.left?t.left._count:0)+(t.right?t.right._count:0)}function s(t,e){this._compare=t,this.root=e}var l=s.prototype;function c(t,e){var r;if(e.left&&(r=c(t,e.left)))return r;return(r=t(e.key,e.value))||(e.right?c(t,e.right):void 0)}function u(t,e,r,n){if(e(t,n.key)<=0){var i;if(n.left)if(i=u(t,e,r,n.left))return i;if(i=r(n.key,n.value))return i}if(n.right)return u(t,e,r,n.right)}function f(t,e,r,n,i){var a,o=r(t,i.key),s=r(e,i.key);if(o<=0){if(i.left&&(a=f(t,e,r,n,i.left)))return a;if(s>0&&(a=n(i.key,i.value)))return a}if(s>0&&i.right)return f(t,e,r,n,i.right)}function h(t,e){this.tree=t,this._stack=e}Object.defineProperty(l,"keys",{get:function(){var t=[];return this.forEach((function(e,r){t.push(e)})),t}}),Object.defineProperty(l,"values",{get:function(){var t=[];return this.forEach((function(e,r){t.push(r)})),t}}),Object.defineProperty(l,"length",{get:function(){return this.root?this.root._count:0}}),l.insert=function(t,e){for(var r=this._compare,i=this.root,l=[],c=[];i;){var u=r(t,i.key);l.push(i),c.push(u),i=u<=0?i.left:i.right}l.push(new n(0,t,e,null,null,1));for(var f=l.length-2;f>=0;--f){i=l[f];c[f]<=0?l[f]=new n(i._color,i.key,i.value,l[f+1],i.right,i._count+1):l[f]=new n(i._color,i.key,i.value,i.left,l[f+1],i._count+1)}for(f=l.length-1;f>1;--f){var h=l[f-1];i=l[f];if(1===h._color||1===i._color)break;var p=l[f-2];if(p.left===h)if(h.left===i){if(!(d=p.right)||0!==d._color){if(p._color=0,p.left=h.right,h._color=1,h.right=p,l[f-2]=h,l[f-1]=i,o(p),o(h),f>=3)(m=l[f-3]).left===p?m.left=h:m.right=h;break}h._color=1,p.right=a(1,d),p._color=0,f-=1}else{if(!(d=p.right)||0!==d._color){if(h.right=i.left,p._color=0,p.left=i.right,i._color=1,i.left=h,i.right=p,l[f-2]=i,l[f-1]=h,o(p),o(h),o(i),f>=3)(m=l[f-3]).left===p?m.left=i:m.right=i;break}h._color=1,p.right=a(1,d),p._color=0,f-=1}else if(h.right===i){if(!(d=p.left)||0!==d._color){if(p._color=0,p.right=h.left,h._color=1,h.left=p,l[f-2]=h,l[f-1]=i,o(p),o(h),f>=3)(m=l[f-3]).right===p?m.right=h:m.left=h;break}h._color=1,p.left=a(1,d),p._color=0,f-=1}else{var d;if(!(d=p.left)||0!==d._color){var m;if(h.left=i.right,p._color=0,p.right=i.left,i._color=1,i.right=h,i.left=p,l[f-2]=i,l[f-1]=h,o(p),o(h),o(i),f>=3)(m=l[f-3]).right===p?m.right=i:m.left=i;break}h._color=1,p.left=a(1,d),p._color=0,f-=1}}return l[0]._color=1,new s(r,l[0])},l.forEach=function(t,e,r){if(this.root)switch(arguments.length){case 1:return c(t,this.root);case 2:return u(e,this._compare,t,this.root);case 3:if(this._compare(e,r)>=0)return;return f(e,r,this._compare,t,this.root)}},Object.defineProperty(l,"begin",{get:function(){for(var t=[],e=this.root;e;)t.push(e),e=e.left;return new h(this,t)}}),Object.defineProperty(l,"end",{get:function(){for(var t=[],e=this.root;e;)t.push(e),e=e.right;return new h(this,t)}}),l.at=function(t){if(t<0)return new h(this,[]);for(var e=this.root,r=[];;){if(r.push(e),e.left){if(t=e.right._count)break;e=e.right}return new h(this,[])},l.ge=function(t){for(var e=this._compare,r=this.root,n=[],i=0;r;){var a=e(t,r.key);n.push(r),a<=0&&(i=n.length),r=a<=0?r.left:r.right}return n.length=i,new h(this,n)},l.gt=function(t){for(var e=this._compare,r=this.root,n=[],i=0;r;){var a=e(t,r.key);n.push(r),a<0&&(i=n.length),r=a<0?r.left:r.right}return n.length=i,new h(this,n)},l.lt=function(t){for(var e=this._compare,r=this.root,n=[],i=0;r;){var a=e(t,r.key);n.push(r),a>0&&(i=n.length),r=a<=0?r.left:r.right}return n.length=i,new h(this,n)},l.le=function(t){for(var e=this._compare,r=this.root,n=[],i=0;r;){var a=e(t,r.key);n.push(r),a>=0&&(i=n.length),r=a<0?r.left:r.right}return n.length=i,new h(this,n)},l.find=function(t){for(var e=this._compare,r=this.root,n=[];r;){var i=e(t,r.key);if(n.push(r),0===i)return new h(this,n);r=i<=0?r.left:r.right}return new h(this,[])},l.remove=function(t){var e=this.find(t);return e?e.remove():this},l.get=function(t){for(var e=this._compare,r=this.root;r;){var n=e(t,r.key);if(0===n)return r.value;r=n<=0?r.left:r.right}};var p=h.prototype;function d(t,e){t.key=e.key,t.value=e.value,t.left=e.left,t.right=e.right,t._color=e._color,t._count=e._count}function m(t,e){return te?1:0}Object.defineProperty(p,"valid",{get:function(){return this._stack.length>0}}),Object.defineProperty(p,"node",{get:function(){return this._stack.length>0?this._stack[this._stack.length-1]:null},enumerable:!0}),p.clone=function(){return new h(this.tree,this._stack.slice())},p.remove=function(){var t=this._stack;if(0===t.length)return this.tree;var e=new Array(t.length),r=t[t.length-1];e[e.length-1]=new n(r._color,r.key,r.value,r.left,r.right,r._count);for(var l=t.length-2;l>=0;--l){(r=t[l]).left===t[l+1]?e[l]=new n(r._color,r.key,r.value,e[l+1],r.right,r._count):e[l]=new n(r._color,r.key,r.value,r.left,e[l+1],r._count)}if((r=e[e.length-1]).left&&r.right){var c=e.length;for(r=r.left;r.right;)e.push(r),r=r.right;var u=e[c-1];e.push(new n(r._color,u.key,u.value,r.left,r.right,r._count)),e[c-1].key=r.key,e[c-1].value=r.value;for(l=e.length-2;l>=c;--l)r=e[l],e[l]=new n(r._color,r.key,r.value,r.left,e[l+1],r._count);e[c-1].left=e[c]}if(0===(r=e[e.length-1])._color){var f=e[e.length-2];f.left===r?f.left=null:f.right===r&&(f.right=null),e.pop();for(l=0;l=0;--l){if(e=t[l],0===l)return void(e._color=1);if((r=t[l-1]).left===e){if((n=r.right).right&&0===n.right._color){if(s=(n=r.right=i(n)).right=i(n.right),r.right=n.left,n.left=r,n.right=s,n._color=r._color,e._color=1,r._color=1,s._color=1,o(r),o(n),l>1)(c=t[l-2]).left===r?c.left=n:c.right=n;return void(t[l-1]=n)}if(n.left&&0===n.left._color){if(s=(n=r.right=i(n)).left=i(n.left),r.right=s.left,n.left=s.right,s.left=r,s.right=n,s._color=r._color,r._color=1,n._color=1,e._color=1,o(r),o(n),o(s),l>1)(c=t[l-2]).left===r?c.left=s:c.right=s;return void(t[l-1]=s)}if(1===n._color){if(0===r._color)return r._color=1,void(r.right=a(0,n));r.right=a(0,n);continue}n=i(n),r.right=n.left,n.left=r,n._color=r._color,r._color=0,o(r),o(n),l>1&&((c=t[l-2]).left===r?c.left=n:c.right=n),t[l-1]=n,t[l]=r,l+11)(c=t[l-2]).right===r?c.right=n:c.left=n;return void(t[l-1]=n)}if(n.right&&0===n.right._color){if(s=(n=r.left=i(n)).right=i(n.right),r.left=s.right,n.right=s.left,s.right=r,s.left=n,s._color=r._color,r._color=1,n._color=1,e._color=1,o(r),o(n),o(s),l>1)(c=t[l-2]).right===r?c.right=s:c.left=s;return void(t[l-1]=s)}if(1===n._color){if(0===r._color)return r._color=1,void(r.left=a(0,n));r.left=a(0,n);continue}var c;n=i(n),r.left=n.right,n.right=r,n._color=r._color,r._color=0,o(r),o(n),l>1&&((c=t[l-2]).right===r?c.right=n:c.left=n),t[l-1]=n,t[l]=r,l+10)return this._stack[this._stack.length-1].key},enumerable:!0}),Object.defineProperty(p,"value",{get:function(){if(this._stack.length>0)return this._stack[this._stack.length-1].value},enumerable:!0}),Object.defineProperty(p,"index",{get:function(){var t=0,e=this._stack;if(0===e.length){var r=this.tree.root;return r?r._count:0}e[e.length-1].left&&(t=e[e.length-1].left._count);for(var n=e.length-2;n>=0;--n)e[n+1]===e[n].right&&(++t,e[n].left&&(t+=e[n].left._count));return t},enumerable:!0}),p.next=function(){var t=this._stack;if(0!==t.length){var e=t[t.length-1];if(e.right)for(e=e.right;e;)t.push(e),e=e.left;else for(t.pop();t.length>0&&t[t.length-1].right===e;)e=t[t.length-1],t.pop()}},Object.defineProperty(p,"hasNext",{get:function(){var t=this._stack;if(0===t.length)return!1;if(t[t.length-1].right)return!0;for(var e=t.length-1;e>0;--e)if(t[e-1].left===t[e])return!0;return!1}}),p.update=function(t){var e=this._stack;if(0===e.length)throw new Error("Can't update empty node!");var r=new Array(e.length),i=e[e.length-1];r[r.length-1]=new n(i._color,i.key,t,i.left,i.right,i._count);for(var a=e.length-2;a>=0;--a)(i=e[a]).left===e[a+1]?r[a]=new n(i._color,i.key,i.value,r[a+1],i.right,i._count):r[a]=new n(i._color,i.key,i.value,i.left,r[a+1],i._count);return new s(this.tree._compare,r[0])},p.prev=function(){var t=this._stack;if(0!==t.length){var e=t[t.length-1];if(e.left)for(e=e.left;e;)t.push(e),e=e.right;else for(t.pop();t.length>0&&t[t.length-1].left===e;)e=t[t.length-1],t.pop()}},Object.defineProperty(p,"hasPrev",{get:function(){var t=this._stack;if(0===t.length)return!1;if(t[t.length-1].left)return!0;for(var e=t.length-1;e>0;--e)if(t[e-1].right===t[e])return!0;return!1}})},{}],70:[function(t,e,r){"use strict";e.exports=function(t,e){var r=new u(t);return r.update(e),r};var n=t("./lib/text.js"),i=t("./lib/lines.js"),a=t("./lib/background.js"),o=t("./lib/cube.js"),s=t("./lib/ticks.js"),l=new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]);function c(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t}function u(t){this.gl=t,this.pixelRatio=1,this.bounds=[[-10,-10,-10],[10,10,10]],this.ticks=[[],[],[]],this.autoTicks=!0,this.tickSpacing=[1,1,1],this.tickEnable=[!0,!0,!0],this.tickFont=["sans-serif","sans-serif","sans-serif"],this.tickSize=[12,12,12],this.tickAngle=[0,0,0],this.tickAlign=["auto","auto","auto"],this.tickColor=[[0,0,0,1],[0,0,0,1],[0,0,0,1]],this.tickPad=[10,10,10],this.lastCubeProps={cubeEdges:[0,0,0],axis:[0,0,0]},this.labels=["x","y","z"],this.labelEnable=[!0,!0,!0],this.labelFont="sans-serif",this.labelSize=[20,20,20],this.labelAngle=[0,0,0],this.labelAlign=["auto","auto","auto"],this.labelColor=[[0,0,0,1],[0,0,0,1],[0,0,0,1]],this.labelPad=[10,10,10],this.lineEnable=[!0,!0,!0],this.lineMirror=[!1,!1,!1],this.lineWidth=[1,1,1],this.lineColor=[[0,0,0,1],[0,0,0,1],[0,0,0,1]],this.lineTickEnable=[!0,!0,!0],this.lineTickMirror=[!1,!1,!1],this.lineTickLength=[0,0,0],this.lineTickWidth=[1,1,1],this.lineTickColor=[[0,0,0,1],[0,0,0,1],[0,0,0,1]],this.gridEnable=[!0,!0,!0],this.gridWidth=[1,1,1],this.gridColor=[[0,0,0,1],[0,0,0,1],[0,0,0,1]],this.zeroEnable=[!0,!0,!0],this.zeroLineColor=[[0,0,0,1],[0,0,0,1],[0,0,0,1]],this.zeroLineWidth=[2,2,2],this.backgroundEnable=[!1,!1,!1],this.backgroundColor=[[.8,.8,.8,.5],[.8,.8,.8,.5],[.8,.8,.8,.5]],this._firstInit=!0,this._text=null,this._lines=null,this._background=a(t)}var f=u.prototype;function h(){this.primalOffset=[0,0,0],this.primalMinor=[0,0,0],this.mirrorOffset=[0,0,0],this.mirrorMinor=[0,0,0]}f.update=function(t){function e(e,r,n){if(n in t){var i,a=t[n],o=this[n];(e?Array.isArray(a)&&Array.isArray(a[0]):Array.isArray(a))?this[n]=i=[r(a[0]),r(a[1]),r(a[2])]:this[n]=i=[r(a),r(a),r(a)];for(var s=0;s<3;++s)if(i[s]!==o[s])return!0}return!1}t=t||{};var r,a=e.bind(this,!1,Number),o=e.bind(this,!1,Boolean),l=e.bind(this,!1,String),c=e.bind(this,!0,(function(t){if(Array.isArray(t)){if(3===t.length)return[+t[0],+t[1],+t[2],1];if(4===t.length)return[+t[0],+t[1],+t[2],+t[3]]}return[0,0,0,1]})),u=!1,f=!1;if("bounds"in t)for(var h=t.bounds,p=0;p<2;++p)for(var d=0;d<3;++d)h[p][d]!==this.bounds[p][d]&&(f=!0),this.bounds[p][d]=h[p][d];if("ticks"in t){r=t.ticks,u=!0,this.autoTicks=!1;for(p=0;p<3;++p)this.tickSpacing[p]=0}else a("tickSpacing")&&(this.autoTicks=!0,f=!0);if(this._firstInit&&("ticks"in t||"tickSpacing"in t||(this.autoTicks=!0),f=!0,u=!0,this._firstInit=!1),f&&this.autoTicks&&(r=s.create(this.bounds,this.tickSpacing),u=!0),u){for(p=0;p<3;++p)r[p].sort((function(t,e){return t.x-e.x}));s.equal(r,this.ticks)?u=!1:this.ticks=r}o("tickEnable"),l("tickFont")&&(u=!0),a("tickSize"),a("tickAngle"),a("tickPad"),c("tickColor");var m=l("labels");l("labelFont")&&(m=!0),o("labelEnable"),a("labelSize"),a("labelPad"),c("labelColor"),o("lineEnable"),o("lineMirror"),a("lineWidth"),c("lineColor"),o("lineTickEnable"),o("lineTickMirror"),a("lineTickLength"),a("lineTickWidth"),c("lineTickColor"),o("gridEnable"),a("gridWidth"),c("gridColor"),o("zeroEnable"),c("zeroLineColor"),a("zeroLineWidth"),o("backgroundEnable"),c("backgroundColor"),this._text?this._text&&(m||u)&&this._text.update(this.bounds,this.labels,this.labelFont,this.ticks,this.tickFont):this._text=n(this.gl,this.bounds,this.labels,this.labelFont,this.ticks,this.tickFont),this._lines&&u&&(this._lines.dispose(),this._lines=null),this._lines||(this._lines=i(this.gl,this.bounds,this.ticks))};var p=[new h,new h,new h];function d(t,e,r,n,i){for(var a=t.primalOffset,o=t.primalMinor,s=t.mirrorOffset,l=t.mirrorMinor,c=n[e],u=0;u<3;++u)if(e!==u){var f=a,h=s,p=o,d=l;c&1<0?(p[u]=-1,d[u]=0):(p[u]=0,d[u]=1)}}var m=[0,0,0],g={model:l,view:l,projection:l,_ortho:!1};f.isOpaque=function(){return!0},f.isTransparent=function(){return!1},f.drawTransparent=function(t){};var v=[0,0,0],y=[0,0,0],x=[0,0,0];f.draw=function(t){t=t||g;for(var e=this.gl,r=t.model||l,n=t.view||l,i=t.projection||l,a=this.bounds,s=t._ortho||!1,u=o(r,n,i,a,s),f=u.cubeEdges,h=u.axis,b=n[12],_=n[13],w=n[14],T=n[15],k=(s?2:1)*this.pixelRatio*(i[3]*b+i[7]*_+i[11]*w+i[15]*T)/e.drawingBufferHeight,A=0;A<3;++A)this.lastCubeProps.cubeEdges[A]=f[A],this.lastCubeProps.axis[A]=h[A];var M=p;for(A=0;A<3;++A)d(p[A],A,this.bounds,f,h);e=this.gl;var S,E=m;for(A=0;A<3;++A)this.backgroundEnable[A]?E[A]=h[A]:E[A]=0;this._background.draw(r,n,i,a,E,this.backgroundColor),this._lines.bind(r,n,i,this);for(A=0;A<3;++A){var L=[0,0,0];h[A]>0?L[A]=a[1][A]:L[A]=a[0][A];for(var C=0;C<2;++C){var P=(A+1+C)%3,I=(A+1+(1^C))%3;this.gridEnable[P]&&this._lines.drawGrid(P,I,this.bounds,L,this.gridColor[P],this.gridWidth[P]*this.pixelRatio)}for(C=0;C<2;++C){P=(A+1+C)%3,I=(A+1+(1^C))%3;this.zeroEnable[I]&&Math.min(a[0][I],a[1][I])<=0&&Math.max(a[0][I],a[1][I])>=0&&this._lines.drawZero(P,I,this.bounds,L,this.zeroLineColor[I],this.zeroLineWidth[I]*this.pixelRatio)}}for(A=0;A<3;++A){this.lineEnable[A]&&this._lines.drawAxisLine(A,this.bounds,M[A].primalOffset,this.lineColor[A],this.lineWidth[A]*this.pixelRatio),this.lineMirror[A]&&this._lines.drawAxisLine(A,this.bounds,M[A].mirrorOffset,this.lineColor[A],this.lineWidth[A]*this.pixelRatio);var O=c(v,M[A].primalMinor),z=c(y,M[A].mirrorMinor),D=this.lineTickLength;for(C=0;C<3;++C){var R=k/r[5*C];O[C]*=D[C]*R,z[C]*=D[C]*R}this.lineTickEnable[A]&&this._lines.drawAxisTicks(A,M[A].primalOffset,O,this.lineTickColor[A],this.lineTickWidth[A]*this.pixelRatio),this.lineTickMirror[A]&&this._lines.drawAxisTicks(A,M[A].mirrorOffset,z,this.lineTickColor[A],this.lineTickWidth[A]*this.pixelRatio)}this._lines.unbind(),this._text.bind(r,n,i,this.pixelRatio);var F,B;function N(t){(B=[0,0,0])[t]=1}function j(t,e,r){var n=(t+1)%3,i=(t+2)%3,a=e[n],o=e[i],s=r[n],l=r[i];a>0&&l>0||a>0&&l<0||a<0&&l>0||a<0&&l<0?N(n):(o>0&&s>0||o>0&&s<0||o<0&&s>0||o<0&&s<0)&&N(i)}for(A=0;A<3;++A){var U=M[A].primalMinor,V=M[A].mirrorMinor,H=c(x,M[A].primalOffset);for(C=0;C<3;++C)this.lineTickEnable[A]&&(H[C]+=k*U[C]*Math.max(this.lineTickLength[C],0)/r[5*C]);var q=[0,0,0];if(q[A]=1,this.tickEnable[A]){-3600===this.tickAngle[A]?(this.tickAngle[A]=0,this.tickAlign[A]="auto"):this.tickAlign[A]=-1,F=1,"auto"===(S=[this.tickAlign[A],.5,F])[0]?S[0]=0:S[0]=parseInt(""+S[0]),B=[0,0,0],j(A,U,V);for(C=0;C<3;++C)H[C]+=k*U[C]*this.tickPad[C]/r[5*C];this._text.drawTicks(A,this.tickSize[A],this.tickAngle[A],H,this.tickColor[A],q,B,S)}if(this.labelEnable[A]){F=0,B=[0,0,0],this.labels[A].length>4&&(N(A),F=1),"auto"===(S=[this.labelAlign[A],.5,F])[0]?S[0]=0:S[0]=parseInt(""+S[0]);for(C=0;C<3;++C)H[C]+=k*U[C]*this.labelPad[C]/r[5*C];H[A]+=.5*(a[0][A]+a[1][A]),this._text.drawLabel(A,this.labelSize[A],this.labelAngle[A],H,this.labelColor[A],[0,0,0],B,S)}}this._text.unbind()},f.dispose=function(){this._text.dispose(),this._lines.dispose(),this._background.dispose(),this._lines=null,this._text=null,this._background=null,this.gl=null}},{"./lib/background.js":71,"./lib/cube.js":72,"./lib/lines.js":73,"./lib/text.js":75,"./lib/ticks.js":76}],71:[function(t,e,r){"use strict";e.exports=function(t){for(var e=[],r=[],s=0,l=0;l<3;++l)for(var c=(l+1)%3,u=(l+2)%3,f=[0,0,0],h=[0,0,0],p=-1;p<=1;p+=2){r.push(s,s+2,s+1,s+1,s+2,s+3),f[l]=p,h[l]=p;for(var d=-1;d<=1;d+=2){f[c]=d;for(var m=-1;m<=1;m+=2)f[u]=m,e.push(f[0],f[1],f[2],h[0],h[1],h[2]),s+=1}var g=c;c=u,u=g}var v=n(t,new Float32Array(e)),y=n(t,new Uint16Array(r),t.ELEMENT_ARRAY_BUFFER),x=i(t,[{buffer:v,type:t.FLOAT,size:3,offset:0,stride:24},{buffer:v,type:t.FLOAT,size:3,offset:12,stride:24}],y),b=a(t);return b.attributes.position.location=0,b.attributes.normal.location=1,new o(t,v,x,b)};var n=t("gl-buffer"),i=t("gl-vao"),a=t("./shaders").bg;function o(t,e,r,n){this.gl=t,this.buffer=e,this.vao=r,this.shader=n}var s=o.prototype;s.draw=function(t,e,r,n,i,a){for(var o=!1,s=0;s<3;++s)o=o||i[s];if(o){var l=this.gl;l.enable(l.POLYGON_OFFSET_FILL),l.polygonOffset(1,2),this.shader.bind(),this.shader.uniforms={model:t,view:e,projection:r,bounds:n,enable:i,colors:a},this.vao.bind(),this.vao.draw(this.gl.TRIANGLES,36),this.vao.unbind(),l.disable(l.POLYGON_OFFSET_FILL)}},s.dispose=function(){this.vao.dispose(),this.buffer.dispose(),this.shader.dispose()}},{"./shaders":74,"gl-buffer":78,"gl-vao":150}],72:[function(t,e,r){"use strict";e.exports=function(t,e,r,a,p){i(s,e,t),i(s,r,s);for(var y=0,x=0;x<2;++x){u[2]=a[x][2];for(var b=0;b<2;++b){u[1]=a[b][1];for(var _=0;_<2;++_)u[0]=a[_][0],h(l[y],u,s),y+=1}}var w=-1;for(x=0;x<8;++x){for(var T=l[x][3],k=0;k<3;++k)c[x][k]=l[x][k]/T;p&&(c[x][2]*=-1),T<0&&(w<0||c[x][2]E&&(w|=1<E&&(w|=1<c[x][1])&&(R=x);var F=-1;for(x=0;x<3;++x){if((N=R^1<c[B][0]&&(B=N)}var j=m;j[0]=j[1]=j[2]=0,j[n.log2(F^R)]=R&F,j[n.log2(R^B)]=R&B;var U=7^B;U===w||U===D?(U=7^F,j[n.log2(B^U)]=U&B):j[n.log2(F^U)]=U&F;var V=g,H=w;for(A=0;A<3;++A)V[A]=H&1< HALF_PI) && (b <= ONE_AND_HALF_PI)) ?\n b - PI :\n b;\n}\n\nfloat look_horizontal_or_vertical(float a, float ratio) {\n // ratio controls the ratio between being horizontal to (vertical + horizontal)\n // if ratio is set to 0.5 then it is 50%, 50%.\n // when using a higher ratio e.g. 0.75 the result would\n // likely be more horizontal than vertical.\n\n float b = positive_angle(a);\n\n return\n (b < ( ratio) * HALF_PI) ? 0.0 :\n (b < (2.0 - ratio) * HALF_PI) ? -HALF_PI :\n (b < (2.0 + ratio) * HALF_PI) ? 0.0 :\n (b < (4.0 - ratio) * HALF_PI) ? HALF_PI :\n 0.0;\n}\n\nfloat roundTo(float a, float b) {\n return float(b * floor((a + 0.5 * b) / b));\n}\n\nfloat look_round_n_directions(float a, int n) {\n float b = positive_angle(a);\n float div = TWO_PI / float(n);\n float c = roundTo(b, div);\n return look_upwards(c);\n}\n\nfloat applyAlignOption(float rawAngle, float delta) {\n return\n (option > 2) ? look_round_n_directions(rawAngle + delta, option) : // option 3-n: round to n directions\n (option == 2) ? look_horizontal_or_vertical(rawAngle + delta, hv_ratio) : // horizontal or vertical\n (option == 1) ? rawAngle + delta : // use free angle, and flip to align with one direction of the axis\n (option == 0) ? look_upwards(rawAngle) : // use free angle, and stay upwards\n (option ==-1) ? 0.0 : // useful for backward compatibility, all texts remains horizontal\n rawAngle; // otherwise return back raw input angle\n}\n\nbool isAxisTitle = (axis.x == 0.0) &&\n (axis.y == 0.0) &&\n (axis.z == 0.0);\n\nvoid main() {\n //Compute world offset\n float axisDistance = position.z;\n vec3 dataPosition = axisDistance * axis + offset;\n\n float beta = angle; // i.e. user defined attributes for each tick\n\n float axisAngle;\n float clipAngle;\n float flip;\n\n if (enableAlign) {\n axisAngle = (isAxisTitle) ? HALF_PI :\n computeViewAngle(dataPosition, dataPosition + axis);\n clipAngle = computeViewAngle(dataPosition, dataPosition + alignDir);\n\n axisAngle += (sin(axisAngle) < 0.0) ? PI : 0.0;\n clipAngle += (sin(clipAngle) < 0.0) ? PI : 0.0;\n\n flip = (dot(vec2(cos(axisAngle), sin(axisAngle)),\n vec2(sin(clipAngle),-cos(clipAngle))) > 0.0) ? 1.0 : 0.0;\n\n beta += applyAlignOption(clipAngle, flip * PI);\n }\n\n //Compute plane offset\n vec2 planeCoord = position.xy * pixelScale;\n\n mat2 planeXform = scale * mat2(\n cos(beta), sin(beta),\n -sin(beta), cos(beta)\n );\n\n vec2 viewOffset = 2.0 * planeXform * planeCoord / resolution;\n\n //Compute clip position\n vec3 clipPosition = project(dataPosition);\n\n //Apply text offset in clip coordinates\n clipPosition += vec3(viewOffset, 0.0);\n\n //Done\n gl_Position = vec4(clipPosition, 1.0);\n}"]),l=n(["precision highp float;\n#define GLSLIFY 1\n\nuniform vec4 color;\nvoid main() {\n gl_FragColor = color;\n}"]);r.text=function(t){return i(t,s,l,null,[{name:"position",type:"vec3"}])};var c=n(["precision highp float;\n#define GLSLIFY 1\n\nattribute vec3 position;\nattribute vec3 normal;\n\nuniform mat4 model, view, projection;\nuniform vec3 enable;\nuniform vec3 bounds[2];\n\nvarying vec3 colorChannel;\n\nvoid main() {\n\n vec3 signAxis = sign(bounds[1] - bounds[0]);\n\n vec3 realNormal = signAxis * normal;\n\n if(dot(realNormal, enable) > 0.0) {\n vec3 minRange = min(bounds[0], bounds[1]);\n vec3 maxRange = max(bounds[0], bounds[1]);\n vec3 nPosition = mix(minRange, maxRange, 0.5 * (position + 1.0));\n gl_Position = projection * view * model * vec4(nPosition, 1.0);\n } else {\n gl_Position = vec4(0,0,0,0);\n }\n\n colorChannel = abs(realNormal);\n}"]),u=n(["precision highp float;\n#define GLSLIFY 1\n\nuniform vec4 colors[3];\n\nvarying vec3 colorChannel;\n\nvoid main() {\n gl_FragColor = colorChannel.x * colors[0] +\n colorChannel.y * colors[1] +\n colorChannel.z * colors[2];\n}"]);r.bg=function(t){return i(t,c,u,null,[{name:"position",type:"vec3"},{name:"normal",type:"vec3"}])}},{"gl-shader":132,glslify:231}],75:[function(t,e,r){(function(r){(function(){"use strict";e.exports=function(t,e,r,a,s,l){var u=n(t),f=i(t,[{buffer:u,size:3}]),h=o(t);h.attributes.position.location=0;var p=new c(t,h,u,f);return p.update(e,r,a,s,l),p};var n=t("gl-buffer"),i=t("gl-vao"),a=t("vectorize-text"),o=t("./shaders").text,s=window||r.global||{},l=s.__TEXT_CACHE||{};s.__TEXT_CACHE={};function c(t,e,r,n){this.gl=t,this.shader=e,this.buffer=r,this.vao=n,this.tickOffset=this.tickCount=this.labelOffset=this.labelCount=null}var u=c.prototype,f=[0,0];u.bind=function(t,e,r,n){this.vao.bind(),this.shader.bind();var i=this.shader.uniforms;i.model=t,i.view=e,i.projection=r,i.pixelScale=n,f[0]=this.gl.drawingBufferWidth,f[1]=this.gl.drawingBufferHeight,this.shader.uniforms.resolution=f},u.unbind=function(){this.vao.unbind()},u.update=function(t,e,r,n,i){var o=[];function s(t,e,r,n,i,s){var c=l[r];c||(c=l[r]={});var u=c[e];u||(u=c[e]=function(t,e){try{return a(t,e)}catch(e){return console.warn('error vectorizing text:"'+t+'" error:',e),{cells:[],positions:[]}}}(e,{triangles:!0,font:r,textAlign:"center",textBaseline:"middle",lineSpacing:i,styletags:s}));for(var f=(n||12)/12,h=u.positions,p=u.cells,d=0,m=p.length;d=0;--v){var y=h[g[v]];o.push(f*y[0],-f*y[1],t)}}for(var c=[0,0,0],u=[0,0,0],f=[0,0,0],h=[0,0,0],p={breaklines:!0,bolds:!0,italics:!0,subscripts:!0,superscripts:!0},d=0;d<3;++d){f[d]=o.length/3|0,s(.5*(t[0][d]+t[1][d]),e[d],r[d],12,1.25,p),h[d]=(o.length/3|0)-f[d],c[d]=o.length/3|0;for(var m=0;m=0&&(i=r.length-n-1);var a=Math.pow(10,i),o=Math.round(t*e*a),s=o+"";if(s.indexOf("e")>=0)return s;var l=o/a,c=o%a;o<0?(l=0|-Math.ceil(l),c=0|-c):(l=0|Math.floor(l),c|=0);var u=""+l;if(o<0&&(u="-"+u),i){for(var f=""+c;f.length=t[0][i];--o)a.push({x:o*e[i],text:n(e[i],o)});r.push(a)}return r},r.equal=function(t,e){for(var r=0;r<3;++r){if(t[r].length!==e[r].length)return!1;for(var n=0;nr)throw new Error("gl-buffer: If resizing buffer, must not specify offset");return t.bufferSubData(e,a,i),r}function u(t,e){for(var r=n.malloc(t.length,e),i=t.length,a=0;a=0;--n){if(e[n]!==r)return!1;r*=t[n]}return!0}(t.shape,t.stride))0===t.offset&&t.data.length===t.shape[0]?this.length=c(this.gl,this.type,this.length,this.usage,t.data,e):this.length=c(this.gl,this.type,this.length,this.usage,t.data.subarray(t.offset,t.shape[0]),e);else{var s=n.malloc(t.size,r),l=a(s,t.shape);i.assign(l,t),this.length=c(this.gl,this.type,this.length,this.usage,e<0?s:s.subarray(0,t.size),e),n.free(s)}}else if(Array.isArray(t)){var f;f=this.type===this.gl.ELEMENT_ARRAY_BUFFER?u(t,"uint16"):u(t,"float32"),this.length=c(this.gl,this.type,this.length,this.usage,e<0?f:f.subarray(0,t.length),e),n.free(f)}else if("object"==typeof t&&"number"==typeof t.length)this.length=c(this.gl,this.type,this.length,this.usage,t,e);else{if("number"!=typeof t&&void 0!==t)throw new Error("gl-buffer: Invalid data type");if(e>=0)throw new Error("gl-buffer: Cannot specify offset when resizing buffer");(t|=0)<=0&&(t=1),this.gl.bufferData(this.type,0|t,this.usage),this.length=t}},e.exports=function(t,e,r,n){if(r=r||t.ARRAY_BUFFER,n=n||t.DYNAMIC_DRAW,r!==t.ARRAY_BUFFER&&r!==t.ELEMENT_ARRAY_BUFFER)throw new Error("gl-buffer: Invalid type for webgl buffer, must be either gl.ARRAY_BUFFER or gl.ELEMENT_ARRAY_BUFFER");if(n!==t.DYNAMIC_DRAW&&n!==t.STATIC_DRAW&&n!==t.STREAM_DRAW)throw new Error("gl-buffer: Invalid usage for buffer, must be either gl.DYNAMIC_DRAW, gl.STATIC_DRAW or gl.STREAM_DRAW");var i=t.createBuffer(),a=new s(t,r,i,0,n);return a.update(e),a}},{ndarray:259,"ndarray-ops":254,"typedarray-pool":308}],79:[function(t,e,r){"use strict";var n=t("gl-vec3");e.exports=function(t,e){var r=t.positions,i=t.vectors,a={positions:[],vertexIntensity:[],vertexIntensityBounds:t.vertexIntensityBounds,vectors:[],cells:[],coneOffset:t.coneOffset,colormap:t.colormap};if(0===t.positions.length)return e&&(e[0]=[0,0,0],e[1]=[0,0,0]),a;for(var o=0,s=1/0,l=-1/0,c=1/0,u=-1/0,f=1/0,h=-1/0,p=null,d=null,m=[],g=1/0,v=!1,y=0;yo&&(o=n.length(b)),y){var _=2*n.distance(p,x)/(n.length(d)+n.length(b));_?(g=Math.min(g,_),v=!1):v=!0}v||(p=x,d=b),m.push(b)}var w=[s,c,f],T=[l,u,h];e&&(e[0]=w,e[1]=T),0===o&&(o=1);var k=1/o;isFinite(g)||(g=1),a.vectorScale=g;var A=t.coneSize||.5;t.absoluteConeSize&&(A=t.absoluteConeSize*k),a.coneScale=A;y=0;for(var M=0;y=1},p.isTransparent=function(){return this.opacity<1},p.pickSlots=1,p.setPickBase=function(t){this.pickId=t},p.update=function(t){t=t||{};var e=this.gl;this.dirty=!0,"lightPosition"in t&&(this.lightPosition=t.lightPosition),"opacity"in t&&(this.opacity=t.opacity),"ambient"in t&&(this.ambientLight=t.ambient),"diffuse"in t&&(this.diffuseLight=t.diffuse),"specular"in t&&(this.specularLight=t.specular),"roughness"in t&&(this.roughness=t.roughness),"fresnel"in t&&(this.fresnel=t.fresnel),void 0!==t.tubeScale&&(this.tubeScale=t.tubeScale),void 0!==t.vectorScale&&(this.vectorScale=t.vectorScale),void 0!==t.coneScale&&(this.coneScale=t.coneScale),void 0!==t.coneOffset&&(this.coneOffset=t.coneOffset),t.colormap&&(this.texture.shape=[256,256],this.texture.minFilter=e.LINEAR_MIPMAP_LINEAR,this.texture.magFilter=e.LINEAR,this.texture.setPixels(function(t){for(var e=u({colormap:t,nshades:256,format:"rgba"}),r=new Uint8Array(1024),n=0;n<256;++n){for(var i=e[n],a=0;a<3;++a)r[4*n+a]=i[a];r[4*n+3]=255*i[3]}return c(r,[256,256,4],[4,0,1])}(t.colormap)),this.texture.generateMipmap());var r=t.cells,n=t.positions,i=t.vectors;if(n&&r&&i){var a=[],o=[],s=[],l=[],f=[];this.cells=r,this.positions=n,this.vectors=i;var h=t.meshColor||[1,1,1,1],p=t.vertexIntensity,d=1/0,m=-1/0;if(p)if(t.vertexIntensityBounds)d=+t.vertexIntensityBounds[0],m=+t.vertexIntensityBounds[1];else for(var g=0;g0){var m=this.triShader;m.bind(),m.uniforms=c,this.triangleVAO.bind(),e.drawArrays(e.TRIANGLES,0,3*this.triangleCount),this.triangleVAO.unbind()}},p.drawPick=function(t){t=t||{};for(var e=this.gl,r=t.model||f,n=t.view||f,i=t.projection||f,a=[[-1e6,-1e6,-1e6],[1e6,1e6,1e6]],o=0;o<3;++o)a[0][o]=Math.max(a[0][o],this.clipBounds[0][o]),a[1][o]=Math.min(a[1][o],this.clipBounds[1][o]);this._model=[].slice.call(r),this._view=[].slice.call(n),this._projection=[].slice.call(i),this._resolution=[e.drawingBufferWidth,e.drawingBufferHeight];var s={model:r,view:n,projection:i,clipBounds:a,tubeScale:this.tubeScale,vectorScale:this.vectorScale,coneScale:this.coneScale,coneOffset:this.coneOffset,pickId:this.pickId/255},l=this.pickShader;l.bind(),l.uniforms=s,this.triangleCount>0&&(this.triangleVAO.bind(),e.drawArrays(e.TRIANGLES,0,3*this.triangleCount),this.triangleVAO.unbind())},p.pick=function(t){if(!t)return null;if(t.id!==this.pickId)return null;var e=t.value[0]+256*t.value[1]+65536*t.value[2],r=this.cells[e],n=this.positions[r[1]].slice(0,3),i={position:n,dataCoordinate:n,index:Math.floor(r[1]/48)};return"cone"===this.traceType?i.index=Math.floor(r[1]/48):"streamtube"===this.traceType&&(i.intensity=this.intensity[r[1]],i.velocity=this.vectors[r[1]].slice(0,3),i.divergence=this.vectors[r[1]][3],i.index=e),i},p.dispose=function(){this.texture.dispose(),this.triShader.dispose(),this.pickShader.dispose(),this.triangleVAO.dispose(),this.trianglePositions.dispose(),this.triangleVectors.dispose(),this.triangleColors.dispose(),this.triangleUVs.dispose(),this.triangleIds.dispose()},e.exports=function(t,e,r){var n=r.shaders;1===arguments.length&&(t=(e=t).gl);var s=d(t,n),l=m(t,n),u=o(t,c(new Uint8Array([255,255,255,255]),[1,1,4]));u.generateMipmap(),u.minFilter=t.LINEAR_MIPMAP_LINEAR,u.magFilter=t.LINEAR;var f=i(t),p=i(t),g=i(t),v=i(t),y=i(t),x=a(t,[{buffer:f,type:t.FLOAT,size:4},{buffer:y,type:t.UNSIGNED_BYTE,size:4,normalized:!0},{buffer:g,type:t.FLOAT,size:4},{buffer:v,type:t.FLOAT,size:2},{buffer:p,type:t.FLOAT,size:4}]),b=new h(t,u,s,l,f,p,y,g,v,x,r.traceType||"cone");return b.update(e),b}},{colormap:53,"gl-buffer":78,"gl-mat4/invert":98,"gl-mat4/multiply":100,"gl-shader":132,"gl-texture2d":146,"gl-vao":150,ndarray:259}],81:[function(t,e,r){var n=t("glslify"),i=n(["precision highp float;\n\nprecision highp float;\n#define GLSLIFY 1\n\nvec3 getOrthogonalVector(vec3 v) {\n // Return up-vector for only-z vector.\n // Return ax + by + cz = 0, a point that lies on the plane that has v as a normal and that isn't (0,0,0).\n // From the above if-statement we have ||a|| > 0 U ||b|| > 0.\n // Assign z = 0, x = -b, y = a:\n // a*-b + b*a + c*0 = -ba + ba + 0 = 0\n if (v.x*v.x > v.z*v.z || v.y*v.y > v.z*v.z) {\n return normalize(vec3(-v.y, v.x, 0.0));\n } else {\n return normalize(vec3(0.0, v.z, -v.y));\n }\n}\n\n// Calculate the cone vertex and normal at the given index.\n//\n// The returned vertex is for a cone with its top at origin and height of 1.0,\n// pointing in the direction of the vector attribute.\n//\n// Each cone is made up of a top vertex, a center base vertex and base perimeter vertices.\n// These vertices are used to make up the triangles of the cone by the following:\n// segment + 0 top vertex\n// segment + 1 perimeter vertex a+1\n// segment + 2 perimeter vertex a\n// segment + 3 center base vertex\n// segment + 4 perimeter vertex a\n// segment + 5 perimeter vertex a+1\n// Where segment is the number of the radial segment * 6 and a is the angle at that radial segment.\n// To go from index to segment, floor(index / 6)\n// To go from segment to angle, 2*pi * (segment/segmentCount)\n// To go from index to segment index, index - (segment*6)\n//\nvec3 getConePosition(vec3 d, float rawIndex, float coneOffset, out vec3 normal) {\n\n const float segmentCount = 8.0;\n\n float index = rawIndex - floor(rawIndex /\n (segmentCount * 6.0)) *\n (segmentCount * 6.0);\n\n float segment = floor(0.001 + index/6.0);\n float segmentIndex = index - (segment*6.0);\n\n normal = -normalize(d);\n\n if (segmentIndex > 2.99 && segmentIndex < 3.01) {\n return mix(vec3(0.0), -d, coneOffset);\n }\n\n float nextAngle = (\n (segmentIndex > 0.99 && segmentIndex < 1.01) ||\n (segmentIndex > 4.99 && segmentIndex < 5.01)\n ) ? 1.0 : 0.0;\n float angle = 2.0 * 3.14159 * ((segment + nextAngle) / segmentCount);\n\n vec3 v1 = mix(d, vec3(0.0), coneOffset);\n vec3 v2 = v1 - d;\n\n vec3 u = getOrthogonalVector(d);\n vec3 v = normalize(cross(u, d));\n\n vec3 x = u * cos(angle) * length(d)*0.25;\n vec3 y = v * sin(angle) * length(d)*0.25;\n vec3 v3 = v2 + x + y;\n if (segmentIndex < 3.0) {\n vec3 tx = u * sin(angle);\n vec3 ty = v * -cos(angle);\n vec3 tangent = tx + ty;\n normal = normalize(cross(v3 - v1, tangent));\n }\n\n if (segmentIndex == 0.0) {\n return mix(d, vec3(0.0), coneOffset);\n }\n return v3;\n}\n\nattribute vec3 vector;\nattribute vec4 color, position;\nattribute vec2 uv;\n\nuniform float vectorScale, coneScale, coneOffset;\nuniform mat4 model, view, projection, inverseModel;\nuniform vec3 eyePosition, lightPosition;\n\nvarying vec3 f_normal, f_lightDirection, f_eyeDirection, f_data, f_position;\nvarying vec4 f_color;\nvarying vec2 f_uv;\n\nvoid main() {\n // Scale the vector magnitude to stay constant with\n // model & view changes.\n vec3 normal;\n vec3 XYZ = getConePosition(mat3(model) * ((vectorScale * coneScale) * vector), position.w, coneOffset, normal);\n vec4 conePosition = model * vec4(position.xyz, 1.0) + vec4(XYZ, 0.0);\n\n //Lighting geometry parameters\n vec4 cameraCoordinate = view * conePosition;\n cameraCoordinate.xyz /= cameraCoordinate.w;\n f_lightDirection = lightPosition - cameraCoordinate.xyz;\n f_eyeDirection = eyePosition - cameraCoordinate.xyz;\n f_normal = normalize((vec4(normal, 0.0) * inverseModel).xyz);\n\n // vec4 m_position = model * vec4(conePosition, 1.0);\n vec4 t_position = view * conePosition;\n gl_Position = projection * t_position;\n\n f_color = color;\n f_data = conePosition.xyz;\n f_position = position.xyz;\n f_uv = uv;\n}\n"]),a=n(["#extension GL_OES_standard_derivatives : enable\n\nprecision highp float;\n#define GLSLIFY 1\n\nfloat beckmannDistribution(float x, float roughness) {\n float NdotH = max(x, 0.0001);\n float cos2Alpha = NdotH * NdotH;\n float tan2Alpha = (cos2Alpha - 1.0) / cos2Alpha;\n float roughness2 = roughness * roughness;\n float denom = 3.141592653589793 * roughness2 * cos2Alpha * cos2Alpha;\n return exp(tan2Alpha / roughness2) / denom;\n}\n\nfloat cookTorranceSpecular(\n vec3 lightDirection,\n vec3 viewDirection,\n vec3 surfaceNormal,\n float roughness,\n float fresnel) {\n\n float VdotN = max(dot(viewDirection, surfaceNormal), 0.0);\n float LdotN = max(dot(lightDirection, surfaceNormal), 0.0);\n\n //Half angle vector\n vec3 H = normalize(lightDirection + viewDirection);\n\n //Geometric term\n float NdotH = max(dot(surfaceNormal, H), 0.0);\n float VdotH = max(dot(viewDirection, H), 0.000001);\n float LdotH = max(dot(lightDirection, H), 0.000001);\n float G1 = (2.0 * NdotH * VdotN) / VdotH;\n float G2 = (2.0 * NdotH * LdotN) / LdotH;\n float G = min(1.0, min(G1, G2));\n \n //Distribution term\n float D = beckmannDistribution(NdotH, roughness);\n\n //Fresnel term\n float F = pow(1.0 - VdotN, fresnel);\n\n //Multiply terms and done\n return G * F * D / max(3.14159265 * VdotN, 0.000001);\n}\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nuniform vec3 clipBounds[2];\nuniform float roughness, fresnel, kambient, kdiffuse, kspecular, opacity;\nuniform sampler2D texture;\n\nvarying vec3 f_normal, f_lightDirection, f_eyeDirection, f_data, f_position;\nvarying vec4 f_color;\nvarying vec2 f_uv;\n\nvoid main() {\n if (outOfRange(clipBounds[0], clipBounds[1], f_position)) discard;\n vec3 N = normalize(f_normal);\n vec3 L = normalize(f_lightDirection);\n vec3 V = normalize(f_eyeDirection);\n\n if(gl_FrontFacing) {\n N = -N;\n }\n\n float specular = min(1.0, max(0.0, cookTorranceSpecular(L, V, N, roughness, fresnel)));\n float diffuse = min(kambient + kdiffuse * max(dot(N, L), 0.0), 1.0);\n\n vec4 surfaceColor = f_color * texture2D(texture, f_uv);\n vec4 litColor = surfaceColor.a * vec4(diffuse * surfaceColor.rgb + kspecular * vec3(1,1,1) * specular, 1.0);\n\n gl_FragColor = litColor * opacity;\n}\n"]),o=n(["precision highp float;\n\nprecision highp float;\n#define GLSLIFY 1\n\nvec3 getOrthogonalVector(vec3 v) {\n // Return up-vector for only-z vector.\n // Return ax + by + cz = 0, a point that lies on the plane that has v as a normal and that isn't (0,0,0).\n // From the above if-statement we have ||a|| > 0 U ||b|| > 0.\n // Assign z = 0, x = -b, y = a:\n // a*-b + b*a + c*0 = -ba + ba + 0 = 0\n if (v.x*v.x > v.z*v.z || v.y*v.y > v.z*v.z) {\n return normalize(vec3(-v.y, v.x, 0.0));\n } else {\n return normalize(vec3(0.0, v.z, -v.y));\n }\n}\n\n// Calculate the cone vertex and normal at the given index.\n//\n// The returned vertex is for a cone with its top at origin and height of 1.0,\n// pointing in the direction of the vector attribute.\n//\n// Each cone is made up of a top vertex, a center base vertex and base perimeter vertices.\n// These vertices are used to make up the triangles of the cone by the following:\n// segment + 0 top vertex\n// segment + 1 perimeter vertex a+1\n// segment + 2 perimeter vertex a\n// segment + 3 center base vertex\n// segment + 4 perimeter vertex a\n// segment + 5 perimeter vertex a+1\n// Where segment is the number of the radial segment * 6 and a is the angle at that radial segment.\n// To go from index to segment, floor(index / 6)\n// To go from segment to angle, 2*pi * (segment/segmentCount)\n// To go from index to segment index, index - (segment*6)\n//\nvec3 getConePosition(vec3 d, float rawIndex, float coneOffset, out vec3 normal) {\n\n const float segmentCount = 8.0;\n\n float index = rawIndex - floor(rawIndex /\n (segmentCount * 6.0)) *\n (segmentCount * 6.0);\n\n float segment = floor(0.001 + index/6.0);\n float segmentIndex = index - (segment*6.0);\n\n normal = -normalize(d);\n\n if (segmentIndex > 2.99 && segmentIndex < 3.01) {\n return mix(vec3(0.0), -d, coneOffset);\n }\n\n float nextAngle = (\n (segmentIndex > 0.99 && segmentIndex < 1.01) ||\n (segmentIndex > 4.99 && segmentIndex < 5.01)\n ) ? 1.0 : 0.0;\n float angle = 2.0 * 3.14159 * ((segment + nextAngle) / segmentCount);\n\n vec3 v1 = mix(d, vec3(0.0), coneOffset);\n vec3 v2 = v1 - d;\n\n vec3 u = getOrthogonalVector(d);\n vec3 v = normalize(cross(u, d));\n\n vec3 x = u * cos(angle) * length(d)*0.25;\n vec3 y = v * sin(angle) * length(d)*0.25;\n vec3 v3 = v2 + x + y;\n if (segmentIndex < 3.0) {\n vec3 tx = u * sin(angle);\n vec3 ty = v * -cos(angle);\n vec3 tangent = tx + ty;\n normal = normalize(cross(v3 - v1, tangent));\n }\n\n if (segmentIndex == 0.0) {\n return mix(d, vec3(0.0), coneOffset);\n }\n return v3;\n}\n\nattribute vec4 vector;\nattribute vec4 position;\nattribute vec4 id;\n\nuniform mat4 model, view, projection;\nuniform float vectorScale, coneScale, coneOffset;\n\nvarying vec3 f_position;\nvarying vec4 f_id;\n\nvoid main() {\n vec3 normal;\n vec3 XYZ = getConePosition(mat3(model) * ((vectorScale * coneScale) * vector.xyz), position.w, coneOffset, normal);\n vec4 conePosition = model * vec4(position.xyz, 1.0) + vec4(XYZ, 0.0);\n gl_Position = projection * view * conePosition;\n f_id = id;\n f_position = position.xyz;\n}\n"]),s=n(["precision highp float;\n#define GLSLIFY 1\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nuniform vec3 clipBounds[2];\nuniform float pickId;\n\nvarying vec3 f_position;\nvarying vec4 f_id;\n\nvoid main() {\n if (outOfRange(clipBounds[0], clipBounds[1], f_position)) discard;\n\n gl_FragColor = vec4(pickId, f_id.xyz);\n}"]);r.meshShader={vertex:i,fragment:a,attributes:[{name:"position",type:"vec4"},{name:"color",type:"vec4"},{name:"uv",type:"vec2"},{name:"vector",type:"vec3"}]},r.pickShader={vertex:o,fragment:s,attributes:[{name:"position",type:"vec4"},{name:"id",type:"vec4"},{name:"vector",type:"vec3"}]}},{glslify:231}],82:[function(t,e,r){e.exports={0:"NONE",1:"ONE",2:"LINE_LOOP",3:"LINE_STRIP",4:"TRIANGLES",5:"TRIANGLE_STRIP",6:"TRIANGLE_FAN",256:"DEPTH_BUFFER_BIT",512:"NEVER",513:"LESS",514:"EQUAL",515:"LEQUAL",516:"GREATER",517:"NOTEQUAL",518:"GEQUAL",519:"ALWAYS",768:"SRC_COLOR",769:"ONE_MINUS_SRC_COLOR",770:"SRC_ALPHA",771:"ONE_MINUS_SRC_ALPHA",772:"DST_ALPHA",773:"ONE_MINUS_DST_ALPHA",774:"DST_COLOR",775:"ONE_MINUS_DST_COLOR",776:"SRC_ALPHA_SATURATE",1024:"STENCIL_BUFFER_BIT",1028:"FRONT",1029:"BACK",1032:"FRONT_AND_BACK",1280:"INVALID_ENUM",1281:"INVALID_VALUE",1282:"INVALID_OPERATION",1285:"OUT_OF_MEMORY",1286:"INVALID_FRAMEBUFFER_OPERATION",2304:"CW",2305:"CCW",2849:"LINE_WIDTH",2884:"CULL_FACE",2885:"CULL_FACE_MODE",2886:"FRONT_FACE",2928:"DEPTH_RANGE",2929:"DEPTH_TEST",2930:"DEPTH_WRITEMASK",2931:"DEPTH_CLEAR_VALUE",2932:"DEPTH_FUNC",2960:"STENCIL_TEST",2961:"STENCIL_CLEAR_VALUE",2962:"STENCIL_FUNC",2963:"STENCIL_VALUE_MASK",2964:"STENCIL_FAIL",2965:"STENCIL_PASS_DEPTH_FAIL",2966:"STENCIL_PASS_DEPTH_PASS",2967:"STENCIL_REF",2968:"STENCIL_WRITEMASK",2978:"VIEWPORT",3024:"DITHER",3042:"BLEND",3088:"SCISSOR_BOX",3089:"SCISSOR_TEST",3106:"COLOR_CLEAR_VALUE",3107:"COLOR_WRITEMASK",3317:"UNPACK_ALIGNMENT",3333:"PACK_ALIGNMENT",3379:"MAX_TEXTURE_SIZE",3386:"MAX_VIEWPORT_DIMS",3408:"SUBPIXEL_BITS",3410:"RED_BITS",3411:"GREEN_BITS",3412:"BLUE_BITS",3413:"ALPHA_BITS",3414:"DEPTH_BITS",3415:"STENCIL_BITS",3553:"TEXTURE_2D",4352:"DONT_CARE",4353:"FASTEST",4354:"NICEST",5120:"BYTE",5121:"UNSIGNED_BYTE",5122:"SHORT",5123:"UNSIGNED_SHORT",5124:"INT",5125:"UNSIGNED_INT",5126:"FLOAT",5386:"INVERT",5890:"TEXTURE",6401:"STENCIL_INDEX",6402:"DEPTH_COMPONENT",6406:"ALPHA",6407:"RGB",6408:"RGBA",6409:"LUMINANCE",6410:"LUMINANCE_ALPHA",7680:"KEEP",7681:"REPLACE",7682:"INCR",7683:"DECR",7936:"VENDOR",7937:"RENDERER",7938:"VERSION",9728:"NEAREST",9729:"LINEAR",9984:"NEAREST_MIPMAP_NEAREST",9985:"LINEAR_MIPMAP_NEAREST",9986:"NEAREST_MIPMAP_LINEAR",9987:"LINEAR_MIPMAP_LINEAR",10240:"TEXTURE_MAG_FILTER",10241:"TEXTURE_MIN_FILTER",10242:"TEXTURE_WRAP_S",10243:"TEXTURE_WRAP_T",10497:"REPEAT",10752:"POLYGON_OFFSET_UNITS",16384:"COLOR_BUFFER_BIT",32769:"CONSTANT_COLOR",32770:"ONE_MINUS_CONSTANT_COLOR",32771:"CONSTANT_ALPHA",32772:"ONE_MINUS_CONSTANT_ALPHA",32773:"BLEND_COLOR",32774:"FUNC_ADD",32777:"BLEND_EQUATION_RGB",32778:"FUNC_SUBTRACT",32779:"FUNC_REVERSE_SUBTRACT",32819:"UNSIGNED_SHORT_4_4_4_4",32820:"UNSIGNED_SHORT_5_5_5_1",32823:"POLYGON_OFFSET_FILL",32824:"POLYGON_OFFSET_FACTOR",32854:"RGBA4",32855:"RGB5_A1",32873:"TEXTURE_BINDING_2D",32926:"SAMPLE_ALPHA_TO_COVERAGE",32928:"SAMPLE_COVERAGE",32936:"SAMPLE_BUFFERS",32937:"SAMPLES",32938:"SAMPLE_COVERAGE_VALUE",32939:"SAMPLE_COVERAGE_INVERT",32968:"BLEND_DST_RGB",32969:"BLEND_SRC_RGB",32970:"BLEND_DST_ALPHA",32971:"BLEND_SRC_ALPHA",33071:"CLAMP_TO_EDGE",33170:"GENERATE_MIPMAP_HINT",33189:"DEPTH_COMPONENT16",33306:"DEPTH_STENCIL_ATTACHMENT",33635:"UNSIGNED_SHORT_5_6_5",33648:"MIRRORED_REPEAT",33901:"ALIASED_POINT_SIZE_RANGE",33902:"ALIASED_LINE_WIDTH_RANGE",33984:"TEXTURE0",33985:"TEXTURE1",33986:"TEXTURE2",33987:"TEXTURE3",33988:"TEXTURE4",33989:"TEXTURE5",33990:"TEXTURE6",33991:"TEXTURE7",33992:"TEXTURE8",33993:"TEXTURE9",33994:"TEXTURE10",33995:"TEXTURE11",33996:"TEXTURE12",33997:"TEXTURE13",33998:"TEXTURE14",33999:"TEXTURE15",34e3:"TEXTURE16",34001:"TEXTURE17",34002:"TEXTURE18",34003:"TEXTURE19",34004:"TEXTURE20",34005:"TEXTURE21",34006:"TEXTURE22",34007:"TEXTURE23",34008:"TEXTURE24",34009:"TEXTURE25",34010:"TEXTURE26",34011:"TEXTURE27",34012:"TEXTURE28",34013:"TEXTURE29",34014:"TEXTURE30",34015:"TEXTURE31",34016:"ACTIVE_TEXTURE",34024:"MAX_RENDERBUFFER_SIZE",34041:"DEPTH_STENCIL",34055:"INCR_WRAP",34056:"DECR_WRAP",34067:"TEXTURE_CUBE_MAP",34068:"TEXTURE_BINDING_CUBE_MAP",34069:"TEXTURE_CUBE_MAP_POSITIVE_X",34070:"TEXTURE_CUBE_MAP_NEGATIVE_X",34071:"TEXTURE_CUBE_MAP_POSITIVE_Y",34072:"TEXTURE_CUBE_MAP_NEGATIVE_Y",34073:"TEXTURE_CUBE_MAP_POSITIVE_Z",34074:"TEXTURE_CUBE_MAP_NEGATIVE_Z",34076:"MAX_CUBE_MAP_TEXTURE_SIZE",34338:"VERTEX_ATTRIB_ARRAY_ENABLED",34339:"VERTEX_ATTRIB_ARRAY_SIZE",34340:"VERTEX_ATTRIB_ARRAY_STRIDE",34341:"VERTEX_ATTRIB_ARRAY_TYPE",34342:"CURRENT_VERTEX_ATTRIB",34373:"VERTEX_ATTRIB_ARRAY_POINTER",34466:"NUM_COMPRESSED_TEXTURE_FORMATS",34467:"COMPRESSED_TEXTURE_FORMATS",34660:"BUFFER_SIZE",34661:"BUFFER_USAGE",34816:"STENCIL_BACK_FUNC",34817:"STENCIL_BACK_FAIL",34818:"STENCIL_BACK_PASS_DEPTH_FAIL",34819:"STENCIL_BACK_PASS_DEPTH_PASS",34877:"BLEND_EQUATION_ALPHA",34921:"MAX_VERTEX_ATTRIBS",34922:"VERTEX_ATTRIB_ARRAY_NORMALIZED",34930:"MAX_TEXTURE_IMAGE_UNITS",34962:"ARRAY_BUFFER",34963:"ELEMENT_ARRAY_BUFFER",34964:"ARRAY_BUFFER_BINDING",34965:"ELEMENT_ARRAY_BUFFER_BINDING",34975:"VERTEX_ATTRIB_ARRAY_BUFFER_BINDING",35040:"STREAM_DRAW",35044:"STATIC_DRAW",35048:"DYNAMIC_DRAW",35632:"FRAGMENT_SHADER",35633:"VERTEX_SHADER",35660:"MAX_VERTEX_TEXTURE_IMAGE_UNITS",35661:"MAX_COMBINED_TEXTURE_IMAGE_UNITS",35663:"SHADER_TYPE",35664:"FLOAT_VEC2",35665:"FLOAT_VEC3",35666:"FLOAT_VEC4",35667:"INT_VEC2",35668:"INT_VEC3",35669:"INT_VEC4",35670:"BOOL",35671:"BOOL_VEC2",35672:"BOOL_VEC3",35673:"BOOL_VEC4",35674:"FLOAT_MAT2",35675:"FLOAT_MAT3",35676:"FLOAT_MAT4",35678:"SAMPLER_2D",35680:"SAMPLER_CUBE",35712:"DELETE_STATUS",35713:"COMPILE_STATUS",35714:"LINK_STATUS",35715:"VALIDATE_STATUS",35716:"INFO_LOG_LENGTH",35717:"ATTACHED_SHADERS",35718:"ACTIVE_UNIFORMS",35719:"ACTIVE_UNIFORM_MAX_LENGTH",35720:"SHADER_SOURCE_LENGTH",35721:"ACTIVE_ATTRIBUTES",35722:"ACTIVE_ATTRIBUTE_MAX_LENGTH",35724:"SHADING_LANGUAGE_VERSION",35725:"CURRENT_PROGRAM",36003:"STENCIL_BACK_REF",36004:"STENCIL_BACK_VALUE_MASK",36005:"STENCIL_BACK_WRITEMASK",36006:"FRAMEBUFFER_BINDING",36007:"RENDERBUFFER_BINDING",36048:"FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE",36049:"FRAMEBUFFER_ATTACHMENT_OBJECT_NAME",36050:"FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL",36051:"FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE",36053:"FRAMEBUFFER_COMPLETE",36054:"FRAMEBUFFER_INCOMPLETE_ATTACHMENT",36055:"FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT",36057:"FRAMEBUFFER_INCOMPLETE_DIMENSIONS",36061:"FRAMEBUFFER_UNSUPPORTED",36064:"COLOR_ATTACHMENT0",36096:"DEPTH_ATTACHMENT",36128:"STENCIL_ATTACHMENT",36160:"FRAMEBUFFER",36161:"RENDERBUFFER",36162:"RENDERBUFFER_WIDTH",36163:"RENDERBUFFER_HEIGHT",36164:"RENDERBUFFER_INTERNAL_FORMAT",36168:"STENCIL_INDEX8",36176:"RENDERBUFFER_RED_SIZE",36177:"RENDERBUFFER_GREEN_SIZE",36178:"RENDERBUFFER_BLUE_SIZE",36179:"RENDERBUFFER_ALPHA_SIZE",36180:"RENDERBUFFER_DEPTH_SIZE",36181:"RENDERBUFFER_STENCIL_SIZE",36194:"RGB565",36336:"LOW_FLOAT",36337:"MEDIUM_FLOAT",36338:"HIGH_FLOAT",36339:"LOW_INT",36340:"MEDIUM_INT",36341:"HIGH_INT",36346:"SHADER_COMPILER",36347:"MAX_VERTEX_UNIFORM_VECTORS",36348:"MAX_VARYING_VECTORS",36349:"MAX_FRAGMENT_UNIFORM_VECTORS",37440:"UNPACK_FLIP_Y_WEBGL",37441:"UNPACK_PREMULTIPLY_ALPHA_WEBGL",37442:"CONTEXT_LOST_WEBGL",37443:"UNPACK_COLORSPACE_CONVERSION_WEBGL",37444:"BROWSER_DEFAULT_WEBGL"}},{}],83:[function(t,e,r){var n=t("./1.0/numbers");e.exports=function(t){return n[t]}},{"./1.0/numbers":82}],84:[function(t,e,r){"use strict";e.exports=function(t){var e=t.gl,r=n(e),o=i(e,[{buffer:r,type:e.FLOAT,size:3,offset:0,stride:40},{buffer:r,type:e.FLOAT,size:4,offset:12,stride:40},{buffer:r,type:e.FLOAT,size:3,offset:28,stride:40}]),l=a(e);l.attributes.position.location=0,l.attributes.color.location=1,l.attributes.offset.location=2;var c=new s(e,r,o,l);return c.update(t),c};var n=t("gl-buffer"),i=t("gl-vao"),a=t("./shaders/index"),o=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1];function s(t,e,r,n){this.gl=t,this.shader=n,this.buffer=e,this.vao=r,this.pixelRatio=1,this.bounds=[[1/0,1/0,1/0],[-1/0,-1/0,-1/0]],this.clipBounds=[[-1/0,-1/0,-1/0],[1/0,1/0,1/0]],this.lineWidth=[1,1,1],this.capSize=[10,10,10],this.lineCount=[0,0,0],this.lineOffset=[0,0,0],this.opacity=1,this.hasAlpha=!1}var l=s.prototype;function c(t,e){for(var r=0;r<3;++r)t[0][r]=Math.min(t[0][r],e[r]),t[1][r]=Math.max(t[1][r],e[r])}l.isOpaque=function(){return!this.hasAlpha},l.isTransparent=function(){return this.hasAlpha},l.drawTransparent=l.draw=function(t){var e=this.gl,r=this.shader.uniforms;this.shader.bind();var n=r.view=t.view||o,i=r.projection=t.projection||o;r.model=t.model||o,r.clipBounds=this.clipBounds,r.opacity=this.opacity;var a=n[12],s=n[13],l=n[14],c=n[15],u=(t._ortho||!1?2:1)*this.pixelRatio*(i[3]*a+i[7]*s+i[11]*l+i[15]*c)/e.drawingBufferHeight;this.vao.bind();for(var f=0;f<3;++f)e.lineWidth(this.lineWidth[f]*this.pixelRatio),r.capSize=this.capSize[f]*u,this.lineCount[f]&&e.drawArrays(e.LINES,this.lineOffset[f],this.lineCount[f]);this.vao.unbind()};var u=function(){for(var t=new Array(3),e=0;e<3;++e){for(var r=[],n=1;n<=2;++n)for(var i=-1;i<=1;i+=2){var a=[0,0,0];a[(n+e)%3]=i,r.push(a)}t[e]=r}return t}();function f(t,e,r,n){for(var i=u[n],a=0;a0)(m=u.slice())[s]+=p[1][s],i.push(u[0],u[1],u[2],d[0],d[1],d[2],d[3],0,0,0,m[0],m[1],m[2],d[0],d[1],d[2],d[3],0,0,0),c(this.bounds,m),o+=2+f(i,m,d,s)}}this.lineCount[s]=o-this.lineOffset[s]}this.buffer.update(i)}},l.dispose=function(){this.shader.dispose(),this.buffer.dispose(),this.vao.dispose()}},{"./shaders/index":85,"gl-buffer":78,"gl-vao":150}],85:[function(t,e,r){"use strict";var n=t("glslify"),i=t("gl-shader"),a=n(["precision highp float;\n#define GLSLIFY 1\n\nattribute vec3 position, offset;\nattribute vec4 color;\nuniform mat4 model, view, projection;\nuniform float capSize;\nvarying vec4 fragColor;\nvarying vec3 fragPosition;\n\nvoid main() {\n vec4 worldPosition = model * vec4(position, 1.0);\n worldPosition = (worldPosition / worldPosition.w) + vec4(capSize * offset, 0.0);\n gl_Position = projection * view * worldPosition;\n fragColor = color;\n fragPosition = position;\n}"]),o=n(["precision highp float;\n#define GLSLIFY 1\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nuniform vec3 clipBounds[2];\nuniform float opacity;\nvarying vec3 fragPosition;\nvarying vec4 fragColor;\n\nvoid main() {\n if (\n outOfRange(clipBounds[0], clipBounds[1], fragPosition) ||\n fragColor.a * opacity == 0.\n ) discard;\n\n gl_FragColor = opacity * fragColor;\n}"]);e.exports=function(t){return i(t,a,o,null,[{name:"position",type:"vec3"},{name:"color",type:"vec4"},{name:"offset",type:"vec3"}])}},{"gl-shader":132,glslify:231}],86:[function(t,e,r){"use strict";var n=t("gl-texture2d");e.exports=function(t,e,r,n){i||(i=t.FRAMEBUFFER_UNSUPPORTED,a=t.FRAMEBUFFER_INCOMPLETE_ATTACHMENT,o=t.FRAMEBUFFER_INCOMPLETE_DIMENSIONS,s=t.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT);var c=t.getExtension("WEBGL_draw_buffers");!l&&c&&function(t,e){var r=t.getParameter(e.MAX_COLOR_ATTACHMENTS_WEBGL);l=new Array(r+1);for(var n=0;n<=r;++n){for(var i=new Array(r),a=0;au||r<0||r>u)throw new Error("gl-fbo: Parameters are too large for FBO");var f=1;if("color"in(n=n||{})){if((f=Math.max(0|n.color,0))<0)throw new Error("gl-fbo: Must specify a nonnegative number of colors");if(f>1){if(!c)throw new Error("gl-fbo: Multiple draw buffer extension not supported");if(f>t.getParameter(c.MAX_COLOR_ATTACHMENTS_WEBGL))throw new Error("gl-fbo: Context does not support "+f+" draw buffers")}}var h=t.UNSIGNED_BYTE,p=t.getExtension("OES_texture_float");if(n.float&&f>0){if(!p)throw new Error("gl-fbo: Context does not support floating point textures");h=t.FLOAT}else n.preferFloat&&f>0&&p&&(h=t.FLOAT);var m=!0;"depth"in n&&(m=!!n.depth);var g=!1;"stencil"in n&&(g=!!n.stencil);return new d(t,e,r,h,f,m,g,c)};var i,a,o,s,l=null;function c(t){return[t.getParameter(t.FRAMEBUFFER_BINDING),t.getParameter(t.RENDERBUFFER_BINDING),t.getParameter(t.TEXTURE_BINDING_2D)]}function u(t,e){t.bindFramebuffer(t.FRAMEBUFFER,e[0]),t.bindRenderbuffer(t.RENDERBUFFER,e[1]),t.bindTexture(t.TEXTURE_2D,e[2])}function f(t){switch(t){case i:throw new Error("gl-fbo: Framebuffer unsupported");case a:throw new Error("gl-fbo: Framebuffer incomplete attachment");case o:throw new Error("gl-fbo: Framebuffer incomplete dimensions");case s:throw new Error("gl-fbo: Framebuffer incomplete missing attachment");default:throw new Error("gl-fbo: Framebuffer failed for unspecified reason")}}function h(t,e,r,i,a,o){if(!i)return null;var s=n(t,e,r,a,i);return s.magFilter=t.NEAREST,s.minFilter=t.NEAREST,s.mipSamples=1,s.bind(),t.framebufferTexture2D(t.FRAMEBUFFER,o,t.TEXTURE_2D,s.handle,0),s}function p(t,e,r,n,i){var a=t.createRenderbuffer();return t.bindRenderbuffer(t.RENDERBUFFER,a),t.renderbufferStorage(t.RENDERBUFFER,n,e,r),t.framebufferRenderbuffer(t.FRAMEBUFFER,i,t.RENDERBUFFER,a),a}function d(t,e,r,n,i,a,o,s){this.gl=t,this._shape=[0|e,0|r],this._destroyed=!1,this._ext=s,this.color=new Array(i);for(var d=0;d1&&s.drawBuffersWEBGL(l[o]);var y=r.getExtension("WEBGL_depth_texture");y?d?t.depth=h(r,i,a,y.UNSIGNED_INT_24_8_WEBGL,r.DEPTH_STENCIL,r.DEPTH_STENCIL_ATTACHMENT):m&&(t.depth=h(r,i,a,r.UNSIGNED_SHORT,r.DEPTH_COMPONENT,r.DEPTH_ATTACHMENT)):m&&d?t._depth_rb=p(r,i,a,r.DEPTH_STENCIL,r.DEPTH_STENCIL_ATTACHMENT):m?t._depth_rb=p(r,i,a,r.DEPTH_COMPONENT16,r.DEPTH_ATTACHMENT):d&&(t._depth_rb=p(r,i,a,r.STENCIL_INDEX,r.STENCIL_ATTACHMENT));var x=r.checkFramebufferStatus(r.FRAMEBUFFER);if(x!==r.FRAMEBUFFER_COMPLETE){t._destroyed=!0,r.bindFramebuffer(r.FRAMEBUFFER,null),r.deleteFramebuffer(t.handle),t.handle=null,t.depth&&(t.depth.dispose(),t.depth=null),t._depth_rb&&(r.deleteRenderbuffer(t._depth_rb),t._depth_rb=null);for(v=0;vi||r<0||r>i)throw new Error("gl-fbo: Can't resize FBO, invalid dimensions");t._shape[0]=e,t._shape[1]=r;for(var a=c(n),o=0;o>8*p&255;this.pickOffset=r,i.bind();var d=i.uniforms;d.viewTransform=t,d.pickOffset=e,d.shape=this.shape;var m=i.attributes;return this.positionBuffer.bind(),m.position.pointer(),this.weightBuffer.bind(),m.weight.pointer(s.UNSIGNED_BYTE,!1),this.idBuffer.bind(),m.pickId.pointer(s.UNSIGNED_BYTE,!1),s.drawArrays(s.TRIANGLES,0,o),r+this.shape[0]*this.shape[1]}}}(),f.pick=function(t,e,r){var n=this.pickOffset,i=this.shape[0]*this.shape[1];if(r=n+i)return null;var a=r-n,o=this.xData,s=this.yData;return{object:this,pointId:a,dataCoord:[o[a%this.shape[0]],s[a/this.shape[0]|0]]}},f.update=function(t){var e=(t=t||{}).shape||[0,0],r=t.x||i(e[0]),o=t.y||i(e[1]),s=t.z||new Float32Array(e[0]*e[1]),l=!1!==t.zsmooth;this.xData=r,this.yData=o;var c,u,f,p,d=t.colorLevels||[0],m=t.colorValues||[0,0,0,1],g=d.length,v=this.bounds;l?(c=v[0]=r[0],u=v[1]=o[0],f=v[2]=r[r.length-1],p=v[3]=o[o.length-1]):(c=v[0]=r[0]+(r[1]-r[0])/2,u=v[1]=o[0]+(o[1]-o[0])/2,f=v[2]=r[r.length-1]+(r[r.length-1]-r[r.length-2])/2,p=v[3]=o[o.length-1]+(o[o.length-1]-o[o.length-2])/2);var y=1/(f-c),x=1/(p-u),b=e[0],_=e[1];this.shape=[b,_];var w=(l?(b-1)*(_-1):b*_)*(h.length>>>1);this.numVertices=w;for(var T=a.mallocUint8(4*w),k=a.mallocFloat32(2*w),A=a.mallocUint8(2*w),M=a.mallocUint32(w),S=0,E=l?b-1:b,L=l?_-1:_,C=0;C max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nuniform vec3 clipBounds[2];\nuniform sampler2D dashTexture;\nuniform float dashScale;\nuniform float opacity;\n\nvarying vec3 worldPosition;\nvarying float pixelArcLength;\nvarying vec4 fragColor;\n\nvoid main() {\n if (\n outOfRange(clipBounds[0], clipBounds[1], worldPosition) ||\n fragColor.a * opacity == 0.\n ) discard;\n\n float dashWeight = texture2D(dashTexture, vec2(dashScale * pixelArcLength, 0)).r;\n if(dashWeight < 0.5) {\n discard;\n }\n gl_FragColor = fragColor * opacity;\n}\n"]),s=n(["precision highp float;\n#define GLSLIFY 1\n\n#define FLOAT_MAX 1.70141184e38\n#define FLOAT_MIN 1.17549435e-38\n\n// https://github.com/mikolalysenko/glsl-read-float/blob/master/index.glsl\nvec4 packFloat(float v) {\n float av = abs(v);\n\n //Handle special cases\n if(av < FLOAT_MIN) {\n return vec4(0.0, 0.0, 0.0, 0.0);\n } else if(v > FLOAT_MAX) {\n return vec4(127.0, 128.0, 0.0, 0.0) / 255.0;\n } else if(v < -FLOAT_MAX) {\n return vec4(255.0, 128.0, 0.0, 0.0) / 255.0;\n }\n\n vec4 c = vec4(0,0,0,0);\n\n //Compute exponent and mantissa\n float e = floor(log2(av));\n float m = av * pow(2.0, -e) - 1.0;\n\n //Unpack mantissa\n c[1] = floor(128.0 * m);\n m -= c[1] / 128.0;\n c[2] = floor(32768.0 * m);\n m -= c[2] / 32768.0;\n c[3] = floor(8388608.0 * m);\n\n //Unpack exponent\n float ebias = e + 127.0;\n c[0] = floor(ebias / 2.0);\n ebias -= c[0] * 2.0;\n c[1] += floor(ebias) * 128.0;\n\n //Unpack sign bit\n c[0] += 128.0 * step(0.0, -v);\n\n //Scale back to range\n return c / 255.0;\n}\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nuniform float pickId;\nuniform vec3 clipBounds[2];\n\nvarying vec3 worldPosition;\nvarying float pixelArcLength;\nvarying vec4 fragColor;\n\nvoid main() {\n if (outOfRange(clipBounds[0], clipBounds[1], worldPosition)) discard;\n\n gl_FragColor = vec4(pickId/255.0, packFloat(pixelArcLength).xyz);\n}"]),l=[{name:"position",type:"vec3"},{name:"nextPosition",type:"vec3"},{name:"arcLength",type:"float"},{name:"lineWidth",type:"float"},{name:"color",type:"vec4"}];r.createShader=function(t){return i(t,a,o,null,l)},r.createPickShader=function(t){return i(t,a,s,null,l)}},{"gl-shader":132,glslify:231}],91:[function(t,e,r){"use strict";e.exports=function(t){var e=t.gl||t.scene&&t.scene.gl,r=f(e);r.attributes.position.location=0,r.attributes.nextPosition.location=1,r.attributes.arcLength.location=2,r.attributes.lineWidth.location=3,r.attributes.color.location=4;var o=h(e);o.attributes.position.location=0,o.attributes.nextPosition.location=1,o.attributes.arcLength.location=2,o.attributes.lineWidth.location=3,o.attributes.color.location=4;for(var s=n(e),l=i(e,[{buffer:s,size:3,offset:0,stride:48},{buffer:s,size:3,offset:12,stride:48},{buffer:s,size:1,offset:24,stride:48},{buffer:s,size:1,offset:28,stride:48},{buffer:s,size:4,offset:32,stride:48}]),u=c(new Array(1024),[256,1,4]),p=0;p<1024;++p)u.data[p]=255;var d=a(e,u);d.wrap=e.REPEAT;var m=new v(e,r,o,s,l,d);return m.update(t),m};var n=t("gl-buffer"),i=t("gl-vao"),a=t("gl-texture2d"),o=new Uint8Array(4),s=new Float32Array(o.buffer);var l=t("binary-search-bounds"),c=t("ndarray"),u=t("./lib/shaders"),f=u.createShader,h=u.createPickShader,p=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1];function d(t,e){for(var r=0,n=0;n<3;++n){var i=t[n]-e[n];r+=i*i}return Math.sqrt(r)}function m(t){for(var e=[[-1e6,-1e6,-1e6],[1e6,1e6,1e6]],r=0;r<3;++r)e[0][r]=Math.max(t[0][r],e[0][r]),e[1][r]=Math.min(t[1][r],e[1][r]);return e}function g(t,e,r,n){this.arcLength=t,this.position=e,this.index=r,this.dataCoordinate=n}function v(t,e,r,n,i,a){this.gl=t,this.shader=e,this.pickShader=r,this.buffer=n,this.vao=i,this.clipBounds=[[-1/0,-1/0,-1/0],[1/0,1/0,1/0]],this.points=[],this.arcLength=[],this.vertexCount=0,this.bounds=[[0,0,0],[0,0,0]],this.pickId=0,this.lineWidth=1,this.texture=a,this.dashScale=1,this.opacity=1,this.hasAlpha=!1,this.dirty=!0,this.pixelRatio=1}var y=v.prototype;y.isTransparent=function(){return this.hasAlpha},y.isOpaque=function(){return!this.hasAlpha},y.pickSlots=1,y.setPickBase=function(t){this.pickId=t},y.drawTransparent=y.draw=function(t){if(this.vertexCount){var e=this.gl,r=this.shader,n=this.vao;r.bind(),r.uniforms={model:t.model||p,view:t.view||p,projection:t.projection||p,clipBounds:m(this.clipBounds),dashTexture:this.texture.bind(),dashScale:this.dashScale/this.arcLength[this.arcLength.length-1],opacity:this.opacity,screenShape:[e.drawingBufferWidth,e.drawingBufferHeight],pixelRatio:this.pixelRatio},n.bind(),n.draw(e.TRIANGLE_STRIP,this.vertexCount),n.unbind()}},y.drawPick=function(t){if(this.vertexCount){var e=this.gl,r=this.pickShader,n=this.vao;r.bind(),r.uniforms={model:t.model||p,view:t.view||p,projection:t.projection||p,pickId:this.pickId,clipBounds:m(this.clipBounds),screenShape:[e.drawingBufferWidth,e.drawingBufferHeight],pixelRatio:this.pixelRatio},n.bind(),n.draw(e.TRIANGLE_STRIP,this.vertexCount),n.unbind()}},y.update=function(t){var e,r;this.dirty=!0;var n=!!t.connectGaps;"dashScale"in t&&(this.dashScale=t.dashScale),this.hasAlpha=!1,"opacity"in t&&(this.opacity=+t.opacity,this.opacity<1&&(this.hasAlpha=!0));var i=[],a=[],o=[],s=0,u=0,f=[[1/0,1/0,1/0],[-1/0,-1/0,-1/0]],h=t.position||t.positions;if(h){var p=t.color||t.colors||[0,0,0,1],m=t.lineWidth||1,g=!1;t:for(e=1;e0){for(var w=0;w<24;++w)i.push(i[i.length-12]);u+=2,g=!0}continue t}f[0][r]=Math.min(f[0][r],b[r],_[r]),f[1][r]=Math.max(f[1][r],b[r],_[r])}Array.isArray(p[0])?(v=p.length>e-1?p[e-1]:p.length>0?p[p.length-1]:[0,0,0,1],y=p.length>e?p[e]:p.length>0?p[p.length-1]:[0,0,0,1]):v=y=p,3===v.length&&(v=[v[0],v[1],v[2],1]),3===y.length&&(y=[y[0],y[1],y[2],1]),!this.hasAlpha&&v[3]<1&&(this.hasAlpha=!0),x=Array.isArray(m)?m.length>e-1?m[e-1]:m.length>0?m[m.length-1]:[0,0,0,1]:m;var T=s;if(s+=d(b,_),g){for(r=0;r<2;++r)i.push(b[0],b[1],b[2],_[0],_[1],_[2],T,x,v[0],v[1],v[2],v[3]);u+=2,g=!1}i.push(b[0],b[1],b[2],_[0],_[1],_[2],T,x,v[0],v[1],v[2],v[3],b[0],b[1],b[2],_[0],_[1],_[2],T,-x,v[0],v[1],v[2],v[3],_[0],_[1],_[2],b[0],b[1],b[2],s,-x,y[0],y[1],y[2],y[3],_[0],_[1],_[2],b[0],b[1],b[2],s,x,y[0],y[1],y[2],y[3]),u+=4}}if(this.buffer.update(i),a.push(s),o.push(h[h.length-1].slice()),this.bounds=f,this.vertexCount=u,this.points=o,this.arcLength=a,"dashes"in t){var k=t.dashes.slice();for(k.unshift(0),e=1;e1.0001)return null;v+=g[f]}if(Math.abs(v-1)>.001)return null;return[h,s(t,g),g]}},{barycentric:14,"polytope-closest-point/lib/closest_point_2d.js":270}],111:[function(t,e,r){var n=t("glslify"),i=n(["precision highp float;\n#define GLSLIFY 1\n\nattribute vec3 position, normal;\nattribute vec4 color;\nattribute vec2 uv;\n\nuniform mat4 model\n , view\n , projection\n , inverseModel;\nuniform vec3 eyePosition\n , lightPosition;\n\nvarying vec3 f_normal\n , f_lightDirection\n , f_eyeDirection\n , f_data;\nvarying vec4 f_color;\nvarying vec2 f_uv;\n\nvec4 project(vec3 p) {\n return projection * view * model * vec4(p, 1.0);\n}\n\nvoid main() {\n gl_Position = project(position);\n\n //Lighting geometry parameters\n vec4 cameraCoordinate = view * vec4(position , 1.0);\n cameraCoordinate.xyz /= cameraCoordinate.w;\n f_lightDirection = lightPosition - cameraCoordinate.xyz;\n f_eyeDirection = eyePosition - cameraCoordinate.xyz;\n f_normal = normalize((vec4(normal, 0.0) * inverseModel).xyz);\n\n f_color = color;\n f_data = position;\n f_uv = uv;\n}\n"]),a=n(["#extension GL_OES_standard_derivatives : enable\n\nprecision highp float;\n#define GLSLIFY 1\n\nfloat beckmannDistribution(float x, float roughness) {\n float NdotH = max(x, 0.0001);\n float cos2Alpha = NdotH * NdotH;\n float tan2Alpha = (cos2Alpha - 1.0) / cos2Alpha;\n float roughness2 = roughness * roughness;\n float denom = 3.141592653589793 * roughness2 * cos2Alpha * cos2Alpha;\n return exp(tan2Alpha / roughness2) / denom;\n}\n\nfloat cookTorranceSpecular(\n vec3 lightDirection,\n vec3 viewDirection,\n vec3 surfaceNormal,\n float roughness,\n float fresnel) {\n\n float VdotN = max(dot(viewDirection, surfaceNormal), 0.0);\n float LdotN = max(dot(lightDirection, surfaceNormal), 0.0);\n\n //Half angle vector\n vec3 H = normalize(lightDirection + viewDirection);\n\n //Geometric term\n float NdotH = max(dot(surfaceNormal, H), 0.0);\n float VdotH = max(dot(viewDirection, H), 0.000001);\n float LdotH = max(dot(lightDirection, H), 0.000001);\n float G1 = (2.0 * NdotH * VdotN) / VdotH;\n float G2 = (2.0 * NdotH * LdotN) / LdotH;\n float G = min(1.0, min(G1, G2));\n \n //Distribution term\n float D = beckmannDistribution(NdotH, roughness);\n\n //Fresnel term\n float F = pow(1.0 - VdotN, fresnel);\n\n //Multiply terms and done\n return G * F * D / max(3.14159265 * VdotN, 0.000001);\n}\n\n//#pragma glslify: beckmann = require(glsl-specular-beckmann) // used in gl-surface3d\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nuniform vec3 clipBounds[2];\nuniform float roughness\n , fresnel\n , kambient\n , kdiffuse\n , kspecular;\nuniform sampler2D texture;\n\nvarying vec3 f_normal\n , f_lightDirection\n , f_eyeDirection\n , f_data;\nvarying vec4 f_color;\nvarying vec2 f_uv;\n\nvoid main() {\n if (f_color.a == 0.0 ||\n outOfRange(clipBounds[0], clipBounds[1], f_data)\n ) discard;\n\n vec3 N = normalize(f_normal);\n vec3 L = normalize(f_lightDirection);\n vec3 V = normalize(f_eyeDirection);\n\n if(gl_FrontFacing) {\n N = -N;\n }\n\n float specular = min(1.0, max(0.0, cookTorranceSpecular(L, V, N, roughness, fresnel)));\n //float specular = max(0.0, beckmann(L, V, N, roughness)); // used in gl-surface3d\n\n float diffuse = min(kambient + kdiffuse * max(dot(N, L), 0.0), 1.0);\n\n vec4 surfaceColor = vec4(f_color.rgb, 1.0) * texture2D(texture, f_uv);\n vec4 litColor = surfaceColor.a * vec4(diffuse * surfaceColor.rgb + kspecular * vec3(1,1,1) * specular, 1.0);\n\n gl_FragColor = litColor * f_color.a;\n}\n"]),o=n(["precision highp float;\n#define GLSLIFY 1\n\nattribute vec3 position;\nattribute vec4 color;\nattribute vec2 uv;\n\nuniform mat4 model, view, projection;\n\nvarying vec4 f_color;\nvarying vec3 f_data;\nvarying vec2 f_uv;\n\nvoid main() {\n gl_Position = projection * view * model * vec4(position, 1.0);\n f_color = color;\n f_data = position;\n f_uv = uv;\n}"]),s=n(["precision highp float;\n#define GLSLIFY 1\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nuniform vec3 clipBounds[2];\nuniform sampler2D texture;\nuniform float opacity;\n\nvarying vec4 f_color;\nvarying vec3 f_data;\nvarying vec2 f_uv;\n\nvoid main() {\n if (outOfRange(clipBounds[0], clipBounds[1], f_data)) discard;\n\n gl_FragColor = f_color * texture2D(texture, f_uv) * opacity;\n}"]),l=n(["precision highp float;\n#define GLSLIFY 1\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nattribute vec3 position;\nattribute vec4 color;\nattribute vec2 uv;\nattribute float pointSize;\n\nuniform mat4 model, view, projection;\nuniform vec3 clipBounds[2];\n\nvarying vec4 f_color;\nvarying vec2 f_uv;\n\nvoid main() {\n if (outOfRange(clipBounds[0], clipBounds[1], position)) {\n\n gl_Position = vec4(0.0, 0.0 ,0.0 ,0.0);\n } else {\n gl_Position = projection * view * model * vec4(position, 1.0);\n }\n gl_PointSize = pointSize;\n f_color = color;\n f_uv = uv;\n}"]),c=n(["precision highp float;\n#define GLSLIFY 1\n\nuniform sampler2D texture;\nuniform float opacity;\n\nvarying vec4 f_color;\nvarying vec2 f_uv;\n\nvoid main() {\n vec2 pointR = gl_PointCoord.xy - vec2(0.5, 0.5);\n if(dot(pointR, pointR) > 0.25) {\n discard;\n }\n gl_FragColor = f_color * texture2D(texture, f_uv) * opacity;\n}"]),u=n(["precision highp float;\n#define GLSLIFY 1\n\nattribute vec3 position;\nattribute vec4 id;\n\nuniform mat4 model, view, projection;\n\nvarying vec3 f_position;\nvarying vec4 f_id;\n\nvoid main() {\n gl_Position = projection * view * model * vec4(position, 1.0);\n f_id = id;\n f_position = position;\n}"]),f=n(["precision highp float;\n#define GLSLIFY 1\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nuniform vec3 clipBounds[2];\nuniform float pickId;\n\nvarying vec3 f_position;\nvarying vec4 f_id;\n\nvoid main() {\n if (outOfRange(clipBounds[0], clipBounds[1], f_position)) discard;\n\n gl_FragColor = vec4(pickId, f_id.xyz);\n}"]),h=n(["precision highp float;\n#define GLSLIFY 1\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nattribute vec3 position;\nattribute float pointSize;\nattribute vec4 id;\n\nuniform mat4 model, view, projection;\nuniform vec3 clipBounds[2];\n\nvarying vec3 f_position;\nvarying vec4 f_id;\n\nvoid main() {\n if (outOfRange(clipBounds[0], clipBounds[1], position)) {\n\n gl_Position = vec4(0.0, 0.0, 0.0, 0.0);\n } else {\n gl_Position = projection * view * model * vec4(position, 1.0);\n gl_PointSize = pointSize;\n }\n f_id = id;\n f_position = position;\n}"]),p=n(["precision highp float;\n#define GLSLIFY 1\n\nattribute vec3 position;\n\nuniform mat4 model, view, projection;\n\nvoid main() {\n gl_Position = projection * view * model * vec4(position, 1.0);\n}"]),d=n(["precision highp float;\n#define GLSLIFY 1\n\nuniform vec3 contourColor;\n\nvoid main() {\n gl_FragColor = vec4(contourColor, 1.0);\n}\n"]);r.meshShader={vertex:i,fragment:a,attributes:[{name:"position",type:"vec3"},{name:"normal",type:"vec3"},{name:"color",type:"vec4"},{name:"uv",type:"vec2"}]},r.wireShader={vertex:o,fragment:s,attributes:[{name:"position",type:"vec3"},{name:"color",type:"vec4"},{name:"uv",type:"vec2"}]},r.pointShader={vertex:l,fragment:c,attributes:[{name:"position",type:"vec3"},{name:"color",type:"vec4"},{name:"uv",type:"vec2"},{name:"pointSize",type:"float"}]},r.pickShader={vertex:u,fragment:f,attributes:[{name:"position",type:"vec3"},{name:"id",type:"vec4"}]},r.pointPickShader={vertex:h,fragment:f,attributes:[{name:"position",type:"vec3"},{name:"pointSize",type:"float"},{name:"id",type:"vec4"}]},r.contourShader={vertex:p,fragment:d,attributes:[{name:"position",type:"vec3"}]}},{glslify:231}],112:[function(t,e,r){"use strict";var n=t("gl-shader"),i=t("gl-buffer"),a=t("gl-vao"),o=t("gl-texture2d"),s=t("normals"),l=t("gl-mat4/multiply"),c=t("gl-mat4/invert"),u=t("ndarray"),f=t("colormap"),h=t("simplicial-complex-contour"),p=t("typedarray-pool"),d=t("./lib/shaders"),m=t("./lib/closest-point"),g=d.meshShader,v=d.wireShader,y=d.pointShader,x=d.pickShader,b=d.pointPickShader,_=d.contourShader,w=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1];function T(t,e,r,n,i,a,o,s,l,c,u,f,h,p,d,m,g,v,y,x,b,_,T,k,A,M,S){this.gl=t,this.pixelRatio=1,this.cells=[],this.positions=[],this.intensity=[],this.texture=e,this.dirty=!0,this.triShader=r,this.lineShader=n,this.pointShader=i,this.pickShader=a,this.pointPickShader=o,this.contourShader=s,this.trianglePositions=l,this.triangleColors=u,this.triangleNormals=h,this.triangleUVs=f,this.triangleIds=c,this.triangleVAO=p,this.triangleCount=0,this.lineWidth=1,this.edgePositions=d,this.edgeColors=g,this.edgeUVs=v,this.edgeIds=m,this.edgeVAO=y,this.edgeCount=0,this.pointPositions=x,this.pointColors=_,this.pointUVs=T,this.pointSizes=k,this.pointIds=b,this.pointVAO=A,this.pointCount=0,this.contourLineWidth=1,this.contourPositions=M,this.contourVAO=S,this.contourCount=0,this.contourColor=[0,0,0],this.contourEnable=!0,this.pickVertex=!0,this.pickId=1,this.bounds=[[1/0,1/0,1/0],[-1/0,-1/0,-1/0]],this.clipBounds=[[-1/0,-1/0,-1/0],[1/0,1/0,1/0]],this.lightPosition=[1e5,1e5,0],this.ambientLight=.8,this.diffuseLight=.8,this.specularLight=2,this.roughness=.5,this.fresnel=1.5,this.opacity=1,this.hasAlpha=!1,this.opacityscale=!1,this._model=w,this._view=w,this._projection=w,this._resolution=[1,1]}var k=T.prototype;function A(t,e){if(!e)return 1;if(!e.length)return 1;for(var r=0;rt&&r>0){var n=(e[r][0]-t)/(e[r][0]-e[r-1][0]);return e[r][1]*(1-n)+n*e[r-1][1]}}return 1}function M(t){var e=n(t,g.vertex,g.fragment);return e.attributes.position.location=0,e.attributes.color.location=2,e.attributes.uv.location=3,e.attributes.normal.location=4,e}function S(t){var e=n(t,v.vertex,v.fragment);return e.attributes.position.location=0,e.attributes.color.location=2,e.attributes.uv.location=3,e}function E(t){var e=n(t,y.vertex,y.fragment);return e.attributes.position.location=0,e.attributes.color.location=2,e.attributes.uv.location=3,e.attributes.pointSize.location=4,e}function L(t){var e=n(t,x.vertex,x.fragment);return e.attributes.position.location=0,e.attributes.id.location=1,e}function C(t){var e=n(t,b.vertex,b.fragment);return e.attributes.position.location=0,e.attributes.id.location=1,e.attributes.pointSize.location=4,e}function P(t){var e=n(t,_.vertex,_.fragment);return e.attributes.position.location=0,e}k.isOpaque=function(){return!this.hasAlpha},k.isTransparent=function(){return this.hasAlpha},k.pickSlots=1,k.setPickBase=function(t){this.pickId=t},k.highlight=function(t){if(t&&this.contourEnable){for(var e=h(this.cells,this.intensity,t.intensity),r=e.cells,n=e.vertexIds,i=e.vertexWeights,a=r.length,o=p.mallocFloat32(6*a),s=0,l=0;l0&&((f=this.triShader).bind(),f.uniforms=s,this.triangleVAO.bind(),e.drawArrays(e.TRIANGLES,0,3*this.triangleCount),this.triangleVAO.unbind());this.edgeCount>0&&this.lineWidth>0&&((f=this.lineShader).bind(),f.uniforms=s,this.edgeVAO.bind(),e.lineWidth(this.lineWidth*this.pixelRatio),e.drawArrays(e.LINES,0,2*this.edgeCount),this.edgeVAO.unbind());this.pointCount>0&&((f=this.pointShader).bind(),f.uniforms=s,this.pointVAO.bind(),e.drawArrays(e.POINTS,0,this.pointCount),this.pointVAO.unbind());this.contourEnable&&this.contourCount>0&&this.contourLineWidth>0&&((f=this.contourShader).bind(),f.uniforms=s,this.contourVAO.bind(),e.drawArrays(e.LINES,0,this.contourCount),this.contourVAO.unbind())},k.drawPick=function(t){t=t||{};for(var e=this.gl,r=t.model||w,n=t.view||w,i=t.projection||w,a=[[-1e6,-1e6,-1e6],[1e6,1e6,1e6]],o=0;o<3;++o)a[0][o]=Math.max(a[0][o],this.clipBounds[0][o]),a[1][o]=Math.min(a[1][o],this.clipBounds[1][o]);this._model=[].slice.call(r),this._view=[].slice.call(n),this._projection=[].slice.call(i),this._resolution=[e.drawingBufferWidth,e.drawingBufferHeight];var s,l={model:r,view:n,projection:i,clipBounds:a,pickId:this.pickId/255};((s=this.pickShader).bind(),s.uniforms=l,this.triangleCount>0&&(this.triangleVAO.bind(),e.drawArrays(e.TRIANGLES,0,3*this.triangleCount),this.triangleVAO.unbind()),this.edgeCount>0&&(this.edgeVAO.bind(),e.lineWidth(this.lineWidth*this.pixelRatio),e.drawArrays(e.LINES,0,2*this.edgeCount),this.edgeVAO.unbind()),this.pointCount>0)&&((s=this.pointPickShader).bind(),s.uniforms=l,this.pointVAO.bind(),e.drawArrays(e.POINTS,0,this.pointCount),this.pointVAO.unbind())},k.pick=function(t){if(!t)return null;if(t.id!==this.pickId)return null;for(var e=t.value[0]+256*t.value[1]+65536*t.value[2],r=this.cells[e],n=this.positions,i=new Array(r.length),a=0;ai[k]&&(r.uniforms.dataAxis=c,r.uniforms.screenOffset=u,r.uniforms.color=g[t],r.uniforms.angle=v[t],a.drawArrays(a.TRIANGLES,i[k],i[A]-i[k]))),y[t]&&T&&(u[1^t]-=M*p*x[t],r.uniforms.dataAxis=f,r.uniforms.screenOffset=u,r.uniforms.color=b[t],r.uniforms.angle=_[t],a.drawArrays(a.TRIANGLES,w,T)),u[1^t]=M*s[2+(1^t)]-1,d[t+2]&&(u[1^t]+=M*p*m[t+2],ki[k]&&(r.uniforms.dataAxis=c,r.uniforms.screenOffset=u,r.uniforms.color=g[t+2],r.uniforms.angle=v[t+2],a.drawArrays(a.TRIANGLES,i[k],i[A]-i[k]))),y[t+2]&&T&&(u[1^t]+=M*p*x[t+2],r.uniforms.dataAxis=f,r.uniforms.screenOffset=u,r.uniforms.color=b[t+2],r.uniforms.angle=_[t+2],a.drawArrays(a.TRIANGLES,w,T))}),m.drawTitle=function(){var t=[0,0],e=[0,0];return function(){var r=this.plot,n=this.shader,i=r.gl,a=r.screenBox,o=r.titleCenter,s=r.titleAngle,l=r.titleColor,c=r.pixelRatio;if(this.titleCount){for(var u=0;u<2;++u)e[u]=2*(o[u]*c-a[u])/(a[2+u]-a[u])-1;n.bind(),n.uniforms.dataAxis=t,n.uniforms.screenOffset=e,n.uniforms.angle=s,n.uniforms.color=l,i.drawArrays(i.TRIANGLES,this.titleOffset,this.titleCount)}}}(),m.bind=(h=[0,0],p=[0,0],d=[0,0],function(){var t=this.plot,e=this.shader,r=t._tickBounds,n=t.dataBox,i=t.screenBox,a=t.viewBox;e.bind();for(var o=0;o<2;++o){var s=r[o],l=r[o+2]-s,c=.5*(n[o+2]+n[o]),u=n[o+2]-n[o],f=a[o],m=a[o+2]-f,g=i[o],v=i[o+2]-g;p[o]=2*l/u*m/v,h[o]=2*(s-c)/u*m/v}d[1]=2*t.pixelRatio/(i[3]-i[1]),d[0]=d[1]*(i[3]-i[1])/(i[2]-i[0]),e.uniforms.dataScale=p,e.uniforms.dataShift=h,e.uniforms.textScale=d,this.vbo.bind(),e.attributes.textCoordinate.pointer()}),m.update=function(t){var e,r,n,i,o,s=[],l=t.ticks,c=t.bounds;for(o=0;o<2;++o){var u=[Math.floor(s.length/3)],f=[-1/0],h=l[o];for(e=0;e=0){var m=e[d]-n[d]*(e[d+2]-e[d])/(n[d+2]-n[d]);0===d?o.drawLine(m,e[1],m,e[3],p[d],h[d]):o.drawLine(e[0],m,e[2],m,p[d],h[d])}}for(d=0;d=0;--t)this.objects[t].dispose();this.objects.length=0;for(t=this.overlays.length-1;t>=0;--t)this.overlays[t].dispose();this.overlays.length=0,this.gl=null},c.addObject=function(t){this.objects.indexOf(t)<0&&(this.objects.push(t),this.setDirty())},c.removeObject=function(t){for(var e=this.objects,r=0;rMath.abs(e))c.rotate(a,0,0,-t*r*Math.PI*d.rotateSpeed/window.innerWidth);else if(!d._ortho){var o=-d.zoomSpeed*i*e/window.innerHeight*(a-c.lastT())/20;c.pan(a,0,0,f*(Math.exp(o)-1))}}}),!0)},d.enableMouseListeners(),d};var n=t("right-now"),i=t("3d-view"),a=t("mouse-change"),o=t("mouse-wheel"),s=t("mouse-event-offset"),l=t("has-passive-events")},{"3d-view":7,"has-passive-events":232,"mouse-change":247,"mouse-event-offset":248,"mouse-wheel":250,"right-now":278}],120:[function(t,e,r){var n=t("glslify"),i=t("gl-shader"),a=n(["precision mediump float;\n#define GLSLIFY 1\nattribute vec2 position;\nvarying vec2 uv;\nvoid main() {\n uv = position;\n gl_Position = vec4(position, 0, 1);\n}"]),o=n(["precision mediump float;\n#define GLSLIFY 1\n\nuniform sampler2D accumBuffer;\nvarying vec2 uv;\n\nvoid main() {\n vec4 accum = texture2D(accumBuffer, 0.5 * (uv + 1.0));\n gl_FragColor = min(vec4(1,1,1,1), accum);\n}"]);e.exports=function(t){return i(t,a,o,null,[{name:"position",type:"vec2"}])}},{"gl-shader":132,glslify:231}],121:[function(t,e,r){"use strict";var n=t("./camera.js"),i=t("gl-axes3d"),a=t("gl-axes3d/properties"),o=t("gl-spikes3d"),s=t("gl-select-static"),l=t("gl-fbo"),c=t("a-big-triangle"),u=t("mouse-change"),f=t("gl-mat4/perspective"),h=t("gl-mat4/ortho"),p=t("./lib/shader"),d=t("is-mobile")({tablet:!0,featureDetect:!0});function m(){this.mouse=[-1,-1],this.screen=null,this.distance=1/0,this.index=null,this.dataCoordinate=null,this.dataPosition=null,this.object=null,this.data=null}function g(t){var e=Math.round(Math.log(Math.abs(t))/Math.log(10));if(e<0){var r=Math.round(Math.pow(10,-e));return Math.ceil(t*r)/r}if(e>0){r=Math.round(Math.pow(10,e));return Math.ceil(t/r)*r}return Math.ceil(t)}function v(t){return"boolean"!=typeof t||t}e.exports={createScene:function(t){(t=t||{}).camera=t.camera||{};var e=t.canvas;if(!e){if(e=document.createElement("canvas"),t.container)t.container.appendChild(e);else document.body.appendChild(e)}var r=t.gl;r||(t.glOptions&&(d=!!t.glOptions.preserveDrawingBuffer),r=function(t,e){var r=null;try{(r=t.getContext("webgl",e))||(r=t.getContext("experimental-webgl",e))}catch(t){return null}return r}(e,t.glOptions||{premultipliedAlpha:!0,antialias:!0,preserveDrawingBuffer:d}));if(!r)throw new Error("webgl not supported");var y=t.bounds||[[-10,-10,-10],[10,10,10]],x=new m,b=l(r,r.drawingBufferWidth,r.drawingBufferHeight,{preferFloat:!d}),_=p(r),w=t.cameraObject&&!0===t.cameraObject._ortho||t.camera.projection&&"orthographic"===t.camera.projection.type||!1,T={eye:t.camera.eye||[2,0,0],center:t.camera.center||[0,0,0],up:t.camera.up||[0,1,0],zoomMin:t.camera.zoomMax||.1,zoomMax:t.camera.zoomMin||100,mode:t.camera.mode||"turntable",_ortho:w},k=t.axes||{},A=i(r,k);A.enable=!k.disable;var M=t.spikes||{},S=o(r,M),E=[],L=[],C=[],P=[],I=!0,O=!0,z=new Array(16),D=new Array(16),R={view:null,projection:z,model:D,_ortho:!1},F=(O=!0,[r.drawingBufferWidth,r.drawingBufferHeight]),B=t.cameraObject||n(e,T),N={gl:r,contextLost:!1,pixelRatio:t.pixelRatio||1,canvas:e,selection:x,camera:B,axes:A,axesPixels:null,spikes:S,bounds:y,objects:E,shape:F,aspect:t.aspectRatio||[1,1,1],pickRadius:t.pickRadius||10,zNear:t.zNear||.01,zFar:t.zFar||1e3,fovy:t.fovy||Math.PI/4,clearColor:t.clearColor||[0,0,0,0],autoResize:v(t.autoResize),autoBounds:v(t.autoBounds),autoScale:!!t.autoScale,autoCenter:v(t.autoCenter),clipToBounds:v(t.clipToBounds),snapToData:!!t.snapToData,onselect:t.onselect||null,onrender:t.onrender||null,onclick:t.onclick||null,cameraParams:R,oncontextloss:null,mouseListener:null,_stopped:!1,getAspectratio:function(){return{x:this.aspect[0],y:this.aspect[1],z:this.aspect[2]}},setAspectratio:function(t){this.aspect[0]=t.x,this.aspect[1]=t.y,this.aspect[2]=t.z,O=!0},setBounds:function(t,e){this.bounds[0][t]=e.min,this.bounds[1][t]=e.max},setClearColor:function(t){this.clearColor=t},clearRGBA:function(){this.gl.clearColor(this.clearColor[0],this.clearColor[1],this.clearColor[2],this.clearColor[3]),this.gl.clear(this.gl.COLOR_BUFFER_BIT|this.gl.DEPTH_BUFFER_BIT)}},j=[r.drawingBufferWidth/N.pixelRatio|0,r.drawingBufferHeight/N.pixelRatio|0];function U(){if(!N._stopped&&N.autoResize){var t=e.parentNode,r=1,n=1;t&&t!==document.body?(r=t.clientWidth,n=t.clientHeight):(r=window.innerWidth,n=window.innerHeight);var i=0|Math.ceil(r*N.pixelRatio),a=0|Math.ceil(n*N.pixelRatio);if(i!==e.width||a!==e.height){e.width=i,e.height=a;var o=e.style;o.position=o.position||"absolute",o.left="0px",o.top="0px",o.width=r+"px",o.height=n+"px",I=!0}}}N.autoResize&&U();function V(){for(var t=E.length,e=P.length,n=0;n0&&0===C[e-1];)C.pop(),P.pop().dispose()}function H(){if(N.contextLost)return!0;r.isContextLost()&&(N.contextLost=!0,N.mouseListener.enabled=!1,N.selection.object=null,N.oncontextloss&&N.oncontextloss())}window.addEventListener("resize",U),N.update=function(t){N._stopped||(t=t||{},I=!0,O=!0)},N.add=function(t){N._stopped||(t.axes=A,E.push(t),L.push(-1),I=!0,O=!0,V())},N.remove=function(t){if(!N._stopped){var e=E.indexOf(t);e<0||(E.splice(e,1),L.pop(),I=!0,O=!0,V())}},N.dispose=function(){if(!N._stopped&&(N._stopped=!0,window.removeEventListener("resize",U),e.removeEventListener("webglcontextlost",H),N.mouseListener.enabled=!1,!N.contextLost)){A.dispose(),S.dispose();for(var t=0;tx.distance)continue;for(var c=0;c 1.0) {\n discard;\n }\n baseColor = mix(borderColor, color, step(radius, centerFraction));\n gl_FragColor = vec4(baseColor.rgb * baseColor.a, baseColor.a);\n }\n}\n"]),r.pickVertex=n(["precision mediump float;\n#define GLSLIFY 1\n\nattribute vec2 position;\nattribute vec4 pickId;\n\nuniform mat3 matrix;\nuniform float pointSize;\nuniform vec4 pickOffset;\n\nvarying vec4 fragId;\n\nvoid main() {\n vec3 hgPosition = matrix * vec3(position, 1);\n gl_Position = vec4(hgPosition.xy, 0, hgPosition.z);\n gl_PointSize = pointSize;\n\n vec4 id = pickId + pickOffset;\n id.y += floor(id.x / 256.0);\n id.x -= floor(id.x / 256.0) * 256.0;\n\n id.z += floor(id.y / 256.0);\n id.y -= floor(id.y / 256.0) * 256.0;\n\n id.w += floor(id.z / 256.0);\n id.z -= floor(id.z / 256.0) * 256.0;\n\n fragId = id;\n}\n"]),r.pickFragment=n(["precision mediump float;\n#define GLSLIFY 1\n\nvarying vec4 fragId;\n\nvoid main() {\n float radius = length(2.0 * gl_PointCoord.xy - 1.0);\n if(radius > 1.0) {\n discard;\n }\n gl_FragColor = fragId / 255.0;\n}\n"])},{glslify:231}],123:[function(t,e,r){"use strict";var n=t("gl-shader"),i=t("gl-buffer"),a=t("typedarray-pool"),o=t("./lib/shader");function s(t,e,r,n,i){this.plot=t,this.offsetBuffer=e,this.pickBuffer=r,this.shader=n,this.pickShader=i,this.sizeMin=.5,this.sizeMinCap=2,this.sizeMax=20,this.areaRatio=1,this.pointCount=0,this.color=[1,0,0,1],this.borderColor=[0,0,0,1],this.blend=!1,this.pickOffset=0,this.points=null}e.exports=function(t,e){var r=t.gl,a=i(r),l=i(r),c=n(r,o.pointVertex,o.pointFragment),u=n(r,o.pickVertex,o.pickFragment),f=new s(t,a,l,c,u);return f.update(e),t.addObject(f),f};var l,c,u=s.prototype;u.dispose=function(){this.shader.dispose(),this.pickShader.dispose(),this.offsetBuffer.dispose(),this.pickBuffer.dispose(),this.plot.removeObject(this)},u.update=function(t){var e;function r(e,r){return e in t?t[e]:r}t=t||{},this.sizeMin=r("sizeMin",.5),this.sizeMax=r("sizeMax",20),this.color=r("color",[1,0,0,1]).slice(),this.areaRatio=r("areaRatio",1),this.borderColor=r("borderColor",[0,0,0,1]).slice(),this.blend=r("blend",!1);var n=t.positions.length>>>1,i=t.positions instanceof Float32Array,o=t.idToIndex instanceof Int32Array&&t.idToIndex.length>=n,s=t.positions,l=i?s:a.mallocFloat32(s.length),c=o?t.idToIndex:a.mallocInt32(n);if(i||l.set(s),!o)for(l.set(s),e=0;e>>1;for(r=0;r=e[0]&&a<=e[2]&&o>=e[1]&&o<=e[3]&&n++}return n}(this.points,i),u=this.plot.pickPixelRatio*Math.max(Math.min(this.sizeMinCap,this.sizeMin),Math.min(this.sizeMax,this.sizeMax/Math.pow(s,.33333)));l[0]=2/a,l[4]=2/o,l[6]=-2*i[0]/a-1,l[7]=-2*i[1]/o-1,this.offsetBuffer.bind(),r.bind(),r.attributes.position.pointer(),r.uniforms.matrix=l,r.uniforms.color=this.color,r.uniforms.borderColor=this.borderColor,r.uniforms.pointCloud=u<5,r.uniforms.pointSize=u,r.uniforms.centerFraction=Math.min(1,Math.max(0,Math.sqrt(1-this.areaRatio))),e&&(c[0]=255&t,c[1]=t>>8&255,c[2]=t>>16&255,c[3]=t>>24&255,this.pickBuffer.bind(),r.attributes.pickId.pointer(n.UNSIGNED_BYTE),r.uniforms.pickOffset=c,this.pickOffset=t);var f=n.getParameter(n.BLEND),h=n.getParameter(n.DITHER);return f&&!this.blend&&n.disable(n.BLEND),h&&n.disable(n.DITHER),n.drawArrays(n.POINTS,0,this.pointCount),f&&!this.blend&&n.enable(n.BLEND),h&&n.enable(n.DITHER),t+this.pointCount}),u.draw=u.unifiedDraw,u.drawPick=u.unifiedDraw,u.pick=function(t,e,r){var n=this.pickOffset,i=this.pointCount;if(r=n+i)return null;var a=r-n,o=this.points;return{object:this,pointId:a,dataCoord:[o[2*a],o[2*a+1]]}}},{"./lib/shader":122,"gl-buffer":78,"gl-shader":132,"typedarray-pool":308}],124:[function(t,e,r){e.exports=function(t,e,r,n){var i,a,o,s,l,c=e[0],u=e[1],f=e[2],h=e[3],p=r[0],d=r[1],m=r[2],g=r[3];(a=c*p+u*d+f*m+h*g)<0&&(a=-a,p=-p,d=-d,m=-m,g=-g);1-a>1e-6?(i=Math.acos(a),o=Math.sin(i),s=Math.sin((1-n)*i)/o,l=Math.sin(n*i)/o):(s=1-n,l=n);return t[0]=s*c+l*p,t[1]=s*u+l*d,t[2]=s*f+l*m,t[3]=s*h+l*g,t}},{}],125:[function(t,e,r){"use strict";e.exports=function(t){return t||0===t?t.toString():""}},{}],126:[function(t,e,r){"use strict";var n=t("vectorize-text");e.exports=function(t,e,r){var a=i[e];a||(a=i[e]={});if(t in a)return a[t];var o={textAlign:"center",textBaseline:"middle",lineHeight:1,font:e,lineSpacing:1.25,styletags:{breaklines:!0,bolds:!0,italics:!0,subscripts:!0,superscripts:!0},triangles:!0},s=n(t,o);o.triangles=!1;var l,c,u=n(t,o);if(r&&1!==r){for(l=0;l max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nattribute vec3 position;\nattribute vec4 color;\nattribute vec2 glyph;\nattribute vec4 id;\n\nuniform vec4 highlightId;\nuniform float highlightScale;\nuniform mat4 model, view, projection;\nuniform vec3 clipBounds[2];\n\nvarying vec4 interpColor;\nvarying vec4 pickId;\nvarying vec3 dataCoordinate;\n\nvoid main() {\n if (outOfRange(clipBounds[0], clipBounds[1], position)) {\n\n gl_Position = vec4(0,0,0,0);\n } else {\n float scale = 1.0;\n if(distance(highlightId, id) < 0.0001) {\n scale = highlightScale;\n }\n\n vec4 worldPosition = model * vec4(position, 1);\n vec4 viewPosition = view * worldPosition;\n viewPosition = viewPosition / viewPosition.w;\n vec4 clipPosition = projection * (viewPosition + scale * vec4(glyph.x, -glyph.y, 0, 0));\n\n gl_Position = clipPosition;\n interpColor = color;\n pickId = id;\n dataCoordinate = position;\n }\n}"]),o=i(["precision highp float;\n#define GLSLIFY 1\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nattribute vec3 position;\nattribute vec4 color;\nattribute vec2 glyph;\nattribute vec4 id;\n\nuniform mat4 model, view, projection;\nuniform vec2 screenSize;\nuniform vec3 clipBounds[2];\nuniform float highlightScale, pixelRatio;\nuniform vec4 highlightId;\n\nvarying vec4 interpColor;\nvarying vec4 pickId;\nvarying vec3 dataCoordinate;\n\nvoid main() {\n if (outOfRange(clipBounds[0], clipBounds[1], position)) {\n\n gl_Position = vec4(0,0,0,0);\n } else {\n float scale = pixelRatio;\n if(distance(highlightId.bgr, id.bgr) < 0.001) {\n scale *= highlightScale;\n }\n\n vec4 worldPosition = model * vec4(position, 1.0);\n vec4 viewPosition = view * worldPosition;\n vec4 clipPosition = projection * viewPosition;\n clipPosition /= clipPosition.w;\n\n gl_Position = clipPosition + vec4(screenSize * scale * vec2(glyph.x, -glyph.y), 0.0, 0.0);\n interpColor = color;\n pickId = id;\n dataCoordinate = position;\n }\n}"]),s=i(["precision highp float;\n#define GLSLIFY 1\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nattribute vec3 position;\nattribute vec4 color;\nattribute vec2 glyph;\nattribute vec4 id;\n\nuniform float highlightScale;\nuniform vec4 highlightId;\nuniform vec3 axes[2];\nuniform mat4 model, view, projection;\nuniform vec2 screenSize;\nuniform vec3 clipBounds[2];\nuniform float scale, pixelRatio;\n\nvarying vec4 interpColor;\nvarying vec4 pickId;\nvarying vec3 dataCoordinate;\n\nvoid main() {\n if (outOfRange(clipBounds[0], clipBounds[1], position)) {\n\n gl_Position = vec4(0,0,0,0);\n } else {\n float lscale = pixelRatio * scale;\n if(distance(highlightId, id) < 0.0001) {\n lscale *= highlightScale;\n }\n\n vec4 clipCenter = projection * view * model * vec4(position, 1);\n vec3 dataPosition = position + 0.5*lscale*(axes[0] * glyph.x + axes[1] * glyph.y) * clipCenter.w * screenSize.y;\n vec4 clipPosition = projection * view * model * vec4(dataPosition, 1);\n\n gl_Position = clipPosition;\n interpColor = color;\n pickId = id;\n dataCoordinate = dataPosition;\n }\n}\n"]),l=i(["precision highp float;\n#define GLSLIFY 1\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nuniform vec3 fragClipBounds[2];\nuniform float opacity;\n\nvarying vec4 interpColor;\nvarying vec3 dataCoordinate;\n\nvoid main() {\n if (\n outOfRange(fragClipBounds[0], fragClipBounds[1], dataCoordinate) ||\n interpColor.a * opacity == 0.\n ) discard;\n gl_FragColor = interpColor * opacity;\n}\n"]),c=i(["precision highp float;\n#define GLSLIFY 1\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nuniform vec3 fragClipBounds[2];\nuniform float pickGroup;\n\nvarying vec4 pickId;\nvarying vec3 dataCoordinate;\n\nvoid main() {\n if (outOfRange(fragClipBounds[0], fragClipBounds[1], dataCoordinate)) discard;\n\n gl_FragColor = vec4(pickGroup, pickId.bgr);\n}"]),u=[{name:"position",type:"vec3"},{name:"color",type:"vec4"},{name:"glyph",type:"vec2"},{name:"id",type:"vec4"}],f={vertex:a,fragment:l,attributes:u},h={vertex:o,fragment:l,attributes:u},p={vertex:s,fragment:l,attributes:u},d={vertex:a,fragment:c,attributes:u},m={vertex:o,fragment:c,attributes:u},g={vertex:s,fragment:c,attributes:u};function v(t,e){var r=n(t,e),i=r.attributes;return i.position.location=0,i.color.location=1,i.glyph.location=2,i.id.location=3,r}r.createPerspective=function(t){return v(t,f)},r.createOrtho=function(t){return v(t,h)},r.createProject=function(t){return v(t,p)},r.createPickPerspective=function(t){return v(t,d)},r.createPickOrtho=function(t){return v(t,m)},r.createPickProject=function(t){return v(t,g)}},{"gl-shader":132,glslify:231}],128:[function(t,e,r){"use strict";var n=t("is-string-blank"),i=t("gl-buffer"),a=t("gl-vao"),o=t("typedarray-pool"),s=t("gl-mat4/multiply"),l=t("./lib/shaders"),c=t("./lib/glyphs"),u=t("./lib/get-simple-string"),f=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1];function h(t,e){var r=t[0],n=t[1],i=t[2],a=t[3];return t[0]=e[0]*r+e[4]*n+e[8]*i+e[12]*a,t[1]=e[1]*r+e[5]*n+e[9]*i+e[13]*a,t[2]=e[2]*r+e[6]*n+e[10]*i+e[14]*a,t[3]=e[3]*r+e[7]*n+e[11]*i+e[15]*a,t}function p(t,e,r,n){return h(n,n),h(n,n),h(n,n)}function d(t,e){this.index=t,this.dataCoordinate=this.position=e}function m(t){return!0===t||t>1?1:t}function g(t,e,r,n,i,a,o,s,l,c,u,f){this.gl=t,this.pixelRatio=1,this.shader=e,this.orthoShader=r,this.projectShader=n,this.pointBuffer=i,this.colorBuffer=a,this.glyphBuffer=o,this.idBuffer=s,this.vao=l,this.vertexCount=0,this.lineVertexCount=0,this.opacity=1,this.hasAlpha=!1,this.lineWidth=0,this.projectScale=[2/3,2/3,2/3],this.projectOpacity=[1,1,1],this.projectHasAlpha=!1,this.pickId=0,this.pickPerspectiveShader=c,this.pickOrthoShader=u,this.pickProjectShader=f,this.points=[],this._selectResult=new d(0,[0,0,0]),this.useOrtho=!0,this.bounds=[[1/0,1/0,1/0],[-1/0,-1/0,-1/0]],this.axesProject=[!0,!0,!0],this.axesBounds=[[-1/0,-1/0,-1/0],[1/0,1/0,1/0]],this.highlightId=[1,1,1,1],this.highlightScale=2,this.clipBounds=[[-1/0,-1/0,-1/0],[1/0,1/0,1/0]],this.dirty=!0}e.exports=function(t){var e=t.gl,r=l.createPerspective(e),n=l.createOrtho(e),o=l.createProject(e),s=l.createPickPerspective(e),c=l.createPickOrtho(e),u=l.createPickProject(e),f=i(e),h=i(e),p=i(e),d=i(e),m=a(e,[{buffer:f,size:3,type:e.FLOAT},{buffer:h,size:4,type:e.FLOAT},{buffer:p,size:2,type:e.FLOAT},{buffer:d,size:4,type:e.UNSIGNED_BYTE,normalized:!0}]),v=new g(e,r,n,o,f,h,p,d,m,s,c,u);return v.update(t),v};var v=g.prototype;v.pickSlots=1,v.setPickBase=function(t){this.pickId=t},v.isTransparent=function(){if(this.hasAlpha)return!0;for(var t=0;t<3;++t)if(this.axesProject[t]&&this.projectHasAlpha)return!0;return!1},v.isOpaque=function(){if(!this.hasAlpha)return!0;for(var t=0;t<3;++t)if(this.axesProject[t]&&!this.projectHasAlpha)return!0;return!1};var y=[0,0],x=[0,0,0],b=[0,0,0],_=[0,0,0,1],w=[0,0,0,1],T=f.slice(),k=[0,0,0],A=[[0,0,0],[0,0,0]];function M(t){return t[0]=t[1]=t[2]=0,t}function S(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=1,t}function E(t,e,r,n){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[r]=n,t}function L(t,e,r,n){var i,a=e.axesProject,o=e.gl,l=t.uniforms,c=r.model||f,u=r.view||f,h=r.projection||f,d=e.axesBounds,m=function(t){for(var e=A,r=0;r<2;++r)for(var n=0;n<3;++n)e[r][n]=Math.max(Math.min(t[r][n],1e8),-1e8);return e}(e.clipBounds);i=e.axes&&e.axes.lastCubeProps?e.axes.lastCubeProps.axis:[1,1,1],y[0]=2/o.drawingBufferWidth,y[1]=2/o.drawingBufferHeight,t.bind(),l.view=u,l.projection=h,l.screenSize=y,l.highlightId=e.highlightId,l.highlightScale=e.highlightScale,l.clipBounds=m,l.pickGroup=e.pickId/255,l.pixelRatio=n;for(var g=0;g<3;++g)if(a[g]){l.scale=e.projectScale[g],l.opacity=e.projectOpacity[g];for(var v=T,L=0;L<16;++L)v[L]=0;for(L=0;L<4;++L)v[5*L]=1;v[5*g]=0,i[g]<0?v[12+g]=d[0][g]:v[12+g]=d[1][g],s(v,c,v),l.model=v;var C=(g+1)%3,P=(g+2)%3,I=M(x),O=M(b);I[C]=1,O[P]=1;var z=p(0,0,0,S(_,I)),D=p(0,0,0,S(w,O));if(Math.abs(z[1])>Math.abs(D[1])){var R=z;z=D,D=R,R=I,I=O,O=R;var F=C;C=P,P=F}z[0]<0&&(I[C]=-1),D[1]>0&&(O[P]=-1);var B=0,N=0;for(L=0;L<4;++L)B+=Math.pow(c[4*C+L],2),N+=Math.pow(c[4*P+L],2);I[C]/=Math.sqrt(B),O[P]/=Math.sqrt(N),l.axes[0]=I,l.axes[1]=O,l.fragClipBounds[0]=E(k,m[0],g,-1e8),l.fragClipBounds[1]=E(k,m[1],g,1e8),e.vao.bind(),e.vao.draw(o.TRIANGLES,e.vertexCount),e.lineWidth>0&&(o.lineWidth(e.lineWidth*n),e.vao.draw(o.LINES,e.lineVertexCount,e.vertexCount)),e.vao.unbind()}}var C=[[-1e8,-1e8,-1e8],[1e8,1e8,1e8]];function P(t,e,r,n,i,a,o){var s=r.gl;if((a===r.projectHasAlpha||o)&&L(e,r,n,i),a===r.hasAlpha||o){t.bind();var l=t.uniforms;l.model=n.model||f,l.view=n.view||f,l.projection=n.projection||f,y[0]=2/s.drawingBufferWidth,y[1]=2/s.drawingBufferHeight,l.screenSize=y,l.highlightId=r.highlightId,l.highlightScale=r.highlightScale,l.fragClipBounds=C,l.clipBounds=r.axes.bounds,l.opacity=r.opacity,l.pickGroup=r.pickId/255,l.pixelRatio=i,r.vao.bind(),r.vao.draw(s.TRIANGLES,r.vertexCount),r.lineWidth>0&&(s.lineWidth(r.lineWidth*i),r.vao.draw(s.LINES,r.lineVertexCount,r.vertexCount)),r.vao.unbind()}}function I(t,e,r,i){var a;a=Array.isArray(t)?e=this.pointCount||e<0)return null;var r=this.points[e],n=this._selectResult;n.index=e;for(var i=0;i<3;++i)n.position[i]=n.dataCoordinate[i]=r[i];return n},v.highlight=function(t){if(t){var e=t.index,r=255&e,n=e>>8&255,i=e>>16&255;this.highlightId=[r/255,n/255,i/255,0]}else this.highlightId=[1,1,1,1]},v.update=function(t){if("perspective"in(t=t||{})&&(this.useOrtho=!t.perspective),"orthographic"in t&&(this.useOrtho=!!t.orthographic),"lineWidth"in t&&(this.lineWidth=t.lineWidth),"project"in t)if(Array.isArray(t.project))this.axesProject=t.project;else{var e=!!t.project;this.axesProject=[e,e,e]}if("projectScale"in t)if(Array.isArray(t.projectScale))this.projectScale=t.projectScale.slice();else{var r=+t.projectScale;this.projectScale=[r,r,r]}if(this.projectHasAlpha=!1,"projectOpacity"in t){if(Array.isArray(t.projectOpacity))this.projectOpacity=t.projectOpacity.slice();else{r=+t.projectOpacity;this.projectOpacity=[r,r,r]}for(var n=0;n<3;++n)this.projectOpacity[n]=m(this.projectOpacity[n]),this.projectOpacity[n]<1&&(this.projectHasAlpha=!0)}this.hasAlpha=!1,"opacity"in t&&(this.opacity=m(t.opacity),this.opacity<1&&(this.hasAlpha=!0)),this.dirty=!0;var i,a,s=t.position,l=t.font||"normal",c=t.alignment||[0,0];if(2===c.length)i=c[0],a=c[1];else{i=[],a=[];for(n=0;n0){var O=0,z=x,D=[0,0,0,1],R=[0,0,0,1],F=Array.isArray(p)&&Array.isArray(p[0]),B=Array.isArray(v)&&Array.isArray(v[0]);t:for(n=0;n<_;++n){y+=1;for(w=s[n],T=0;T<3;++T){if(isNaN(w[T])||!isFinite(w[T]))continue t;f[T]=Math.max(f[T],w[T]),u[T]=Math.min(u[T],w[T])}k=(N=I(h,n,l,this.pixelRatio)).mesh,A=N.lines,M=N.bounds;var N,j=N.visible;if(j)if(Array.isArray(p)){if(3===(U=F?n0?1-M[0][0]:Y<0?1+M[1][0]:1,W*=W>0?1-M[0][1]:W<0?1+M[1][1]:1],Z=k.cells||[],J=k.positions||[];for(T=0;T0){var v=r*u;o.drawBox(f-v,h-v,p+v,h+v,a),o.drawBox(f-v,d-v,p+v,d+v,a),o.drawBox(f-v,h-v,f+v,d+v,a),o.drawBox(p-v,h-v,p+v,d+v,a)}}}},s.update=function(t){t=t||{},this.innerFill=!!t.innerFill,this.outerFill=!!t.outerFill,this.innerColor=(t.innerColor||[0,0,0,.5]).slice(),this.outerColor=(t.outerColor||[0,0,0,.5]).slice(),this.borderColor=(t.borderColor||[0,0,0,1]).slice(),this.borderWidth=t.borderWidth||0,this.selectBox=(t.selectBox||this.selectBox).slice()},s.dispose=function(){this.boxBuffer.dispose(),this.boxShader.dispose(),this.plot.removeOverlay(this)}},{"./lib/shaders":129,"gl-buffer":78,"gl-shader":132}],131:[function(t,e,r){"use strict";e.exports=function(t,e){var r=e[0],a=e[1],o=n(t,r,a,{}),s=i.mallocUint8(r*a*4);return new l(t,o,s)};var n=t("gl-fbo"),i=t("typedarray-pool"),a=t("ndarray"),o=t("bit-twiddle").nextPow2;function s(t,e,r,n,i){this.coord=[t,e],this.id=r,this.value=n,this.distance=i}function l(t,e,r){this.gl=t,this.fbo=e,this.buffer=r,this._readTimeout=null;var n=this;this._readCallback=function(){n.gl&&(e.bind(),t.readPixels(0,0,e.shape[0],e.shape[1],t.RGBA,t.UNSIGNED_BYTE,n.buffer),n._readTimeout=null)}}var c=l.prototype;Object.defineProperty(c,"shape",{get:function(){return this.gl?this.fbo.shape.slice():[0,0]},set:function(t){if(this.gl){this.fbo.shape=t;var e=this.fbo.shape[0],r=this.fbo.shape[1];if(r*e*4>this.buffer.length){i.free(this.buffer);for(var n=this.buffer=i.mallocUint8(o(r*e*4)),a=0;ar)for(t=r;te)for(t=e;t=0){for(var T=0|w.type.charAt(w.type.length-1),k=new Array(T),A=0;A=0;)M+=1;_[y]=M}var S=new Array(r.length);function E(){h.program=o.program(p,h._vref,h._fref,b,_);for(var t=0;t=0){if((d=h.charCodeAt(h.length-1)-48)<2||d>4)throw new n("","Invalid data type for attribute "+f+": "+h);s(t,e,p[0],i,d,a,f)}else{if(!(h.indexOf("mat")>=0))throw new n("","Unknown data type for attribute "+f+": "+h);var d;if((d=h.charCodeAt(h.length-1)-48)<2||d>4)throw new n("","Invalid data type for attribute "+f+": "+h);l(t,e,p,i,d,a,f)}}}return a};var n=t("./GLError");function i(t,e,r,n,i,a){this._gl=t,this._wrapper=e,this._index=r,this._locations=n,this._dimension=i,this._constFunc=a}var a=i.prototype;a.pointer=function(t,e,r,n){var i=this._gl,a=this._locations[this._index];i.vertexAttribPointer(a,this._dimension,t||i.FLOAT,!!e,r||0,n||0),i.enableVertexAttribArray(a)},a.set=function(t,e,r,n){return this._constFunc(this._locations[this._index],t,e,r,n)},Object.defineProperty(a,"location",{get:function(){return this._locations[this._index]},set:function(t){return t!==this._locations[this._index]&&(this._locations[this._index]=0|t,this._wrapper.program=null),0|t}});var o=[function(t,e,r){return void 0===r.length?t.vertexAttrib1f(e,r):t.vertexAttrib1fv(e,r)},function(t,e,r,n){return void 0===r.length?t.vertexAttrib2f(e,r,n):t.vertexAttrib2fv(e,r)},function(t,e,r,n,i){return void 0===r.length?t.vertexAttrib3f(e,r,n,i):t.vertexAttrib3fv(e,r)},function(t,e,r,n,i,a){return void 0===r.length?t.vertexAttrib4f(e,r,n,i,a):t.vertexAttrib4fv(e,r)}];function s(t,e,r,n,a,s,l){var c=o[a],u=new i(t,e,r,n,a,c);Object.defineProperty(s,l,{set:function(e){return t.disableVertexAttribArray(n[r]),c(t,n[r],e),e},get:function(){return u},enumerable:!0})}function l(t,e,r,n,i,a,o){for(var l=new Array(i),c=new Array(i),u=0;u4)throw new i("","Invalid uniform dimension type for matrix "+name+": "+v);t["uniformMatrix"+g+"fv"](s[u],!1,f);break}throw new i("","Unknown uniform data type for "+name+": "+v)}if((g=v.charCodeAt(v.length-1)-48)<2||g>4)throw new i("","Invalid data type");switch(v.charAt(0)){case"b":case"i":t["uniform"+g+"iv"](s[u],f);break;case"v":t["uniform"+g+"fv"](s[u],f);break;default:throw new i("","Unrecognized data type for vector "+name+": "+v)}}}}}}function c(t,e,n){if("object"==typeof n){var c=u(n);Object.defineProperty(t,e,{get:a(c),set:l(n),enumerable:!0,configurable:!1})}else s[n]?Object.defineProperty(t,e,{get:(f=n,function(t,e,r){return t.getUniform(e.program,r[f])}),set:l(n),enumerable:!0,configurable:!1}):t[e]=function(t){switch(t){case"bool":return!1;case"int":case"sampler2D":case"samplerCube":case"float":return 0;default:var e=t.indexOf("vec");if(0<=e&&e<=1&&t.length===4+e){if((r=t.charCodeAt(t.length-1)-48)<2||r>4)throw new i("","Invalid data type");return"b"===t.charAt(0)?o(r,!1):o(r,0)}if(0===t.indexOf("mat")&&4===t.length){var r;if((r=t.charCodeAt(t.length-1)-48)<2||r>4)throw new i("","Invalid uniform dimension type for matrix "+name+": "+t);return o(r*r,0)}throw new i("","Unknown uniform data type for "+name+": "+t)}}(r[n].type);var f}function u(t){var e;if(Array.isArray(t)){e=new Array(t.length);for(var r=0;r1){s[0]in a||(a[s[0]]=[]),a=a[s[0]];for(var l=1;l1)for(var l=0;l 0 U ||b|| > 0.\n // Assign z = 0, x = -b, y = a:\n // a*-b + b*a + c*0 = -ba + ba + 0 = 0\n if (v.x*v.x > v.z*v.z || v.y*v.y > v.z*v.z) {\n return normalize(vec3(-v.y, v.x, 0.0));\n } else {\n return normalize(vec3(0.0, v.z, -v.y));\n }\n}\n\n// Calculate the tube vertex and normal at the given index.\n//\n// The returned vertex is for a tube ring with its center at origin, radius of length(d), pointing in the direction of d.\n//\n// Each tube segment is made up of a ring of vertices.\n// These vertices are used to make up the triangles of the tube by connecting them together in the vertex array.\n// The indexes of tube segments run from 0 to 8.\n//\nvec3 getTubePosition(vec3 d, float index, out vec3 normal) {\n float segmentCount = 8.0;\n\n float angle = 2.0 * 3.14159 * (index / segmentCount);\n\n vec3 u = getOrthogonalVector(d);\n vec3 v = normalize(cross(u, d));\n\n vec3 x = u * cos(angle) * length(d);\n vec3 y = v * sin(angle) * length(d);\n vec3 v3 = x + y;\n\n normal = normalize(v3);\n\n return v3;\n}\n\nattribute vec4 vector;\nattribute vec4 color, position;\nattribute vec2 uv;\n\nuniform float vectorScale, tubeScale;\nuniform mat4 model, view, projection, inverseModel;\nuniform vec3 eyePosition, lightPosition;\n\nvarying vec3 f_normal, f_lightDirection, f_eyeDirection, f_data, f_position;\nvarying vec4 f_color;\nvarying vec2 f_uv;\n\nvoid main() {\n // Scale the vector magnitude to stay constant with\n // model & view changes.\n vec3 normal;\n vec3 XYZ = getTubePosition(mat3(model) * (tubeScale * vector.w * normalize(vector.xyz)), position.w, normal);\n vec4 tubePosition = model * vec4(position.xyz, 1.0) + vec4(XYZ, 0.0);\n\n //Lighting geometry parameters\n vec4 cameraCoordinate = view * tubePosition;\n cameraCoordinate.xyz /= cameraCoordinate.w;\n f_lightDirection = lightPosition - cameraCoordinate.xyz;\n f_eyeDirection = eyePosition - cameraCoordinate.xyz;\n f_normal = normalize((vec4(normal, 0.0) * inverseModel).xyz);\n\n // vec4 m_position = model * vec4(tubePosition, 1.0);\n vec4 t_position = view * tubePosition;\n gl_Position = projection * t_position;\n\n f_color = color;\n f_data = tubePosition.xyz;\n f_position = position.xyz;\n f_uv = uv;\n}\n"]),a=n(["#extension GL_OES_standard_derivatives : enable\n\nprecision highp float;\n#define GLSLIFY 1\n\nfloat beckmannDistribution(float x, float roughness) {\n float NdotH = max(x, 0.0001);\n float cos2Alpha = NdotH * NdotH;\n float tan2Alpha = (cos2Alpha - 1.0) / cos2Alpha;\n float roughness2 = roughness * roughness;\n float denom = 3.141592653589793 * roughness2 * cos2Alpha * cos2Alpha;\n return exp(tan2Alpha / roughness2) / denom;\n}\n\nfloat cookTorranceSpecular(\n vec3 lightDirection,\n vec3 viewDirection,\n vec3 surfaceNormal,\n float roughness,\n float fresnel) {\n\n float VdotN = max(dot(viewDirection, surfaceNormal), 0.0);\n float LdotN = max(dot(lightDirection, surfaceNormal), 0.0);\n\n //Half angle vector\n vec3 H = normalize(lightDirection + viewDirection);\n\n //Geometric term\n float NdotH = max(dot(surfaceNormal, H), 0.0);\n float VdotH = max(dot(viewDirection, H), 0.000001);\n float LdotH = max(dot(lightDirection, H), 0.000001);\n float G1 = (2.0 * NdotH * VdotN) / VdotH;\n float G2 = (2.0 * NdotH * LdotN) / LdotH;\n float G = min(1.0, min(G1, G2));\n \n //Distribution term\n float D = beckmannDistribution(NdotH, roughness);\n\n //Fresnel term\n float F = pow(1.0 - VdotN, fresnel);\n\n //Multiply terms and done\n return G * F * D / max(3.14159265 * VdotN, 0.000001);\n}\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nuniform vec3 clipBounds[2];\nuniform float roughness, fresnel, kambient, kdiffuse, kspecular, opacity;\nuniform sampler2D texture;\n\nvarying vec3 f_normal, f_lightDirection, f_eyeDirection, f_data, f_position;\nvarying vec4 f_color;\nvarying vec2 f_uv;\n\nvoid main() {\n if (outOfRange(clipBounds[0], clipBounds[1], f_position)) discard;\n vec3 N = normalize(f_normal);\n vec3 L = normalize(f_lightDirection);\n vec3 V = normalize(f_eyeDirection);\n\n if(gl_FrontFacing) {\n N = -N;\n }\n\n float specular = min(1.0, max(0.0, cookTorranceSpecular(L, V, N, roughness, fresnel)));\n float diffuse = min(kambient + kdiffuse * max(dot(N, L), 0.0), 1.0);\n\n vec4 surfaceColor = f_color * texture2D(texture, f_uv);\n vec4 litColor = surfaceColor.a * vec4(diffuse * surfaceColor.rgb + kspecular * vec3(1,1,1) * specular, 1.0);\n\n gl_FragColor = litColor * opacity;\n}\n"]),o=n(["precision highp float;\n\nprecision highp float;\n#define GLSLIFY 1\n\nvec3 getOrthogonalVector(vec3 v) {\n // Return up-vector for only-z vector.\n // Return ax + by + cz = 0, a point that lies on the plane that has v as a normal and that isn't (0,0,0).\n // From the above if-statement we have ||a|| > 0 U ||b|| > 0.\n // Assign z = 0, x = -b, y = a:\n // a*-b + b*a + c*0 = -ba + ba + 0 = 0\n if (v.x*v.x > v.z*v.z || v.y*v.y > v.z*v.z) {\n return normalize(vec3(-v.y, v.x, 0.0));\n } else {\n return normalize(vec3(0.0, v.z, -v.y));\n }\n}\n\n// Calculate the tube vertex and normal at the given index.\n//\n// The returned vertex is for a tube ring with its center at origin, radius of length(d), pointing in the direction of d.\n//\n// Each tube segment is made up of a ring of vertices.\n// These vertices are used to make up the triangles of the tube by connecting them together in the vertex array.\n// The indexes of tube segments run from 0 to 8.\n//\nvec3 getTubePosition(vec3 d, float index, out vec3 normal) {\n float segmentCount = 8.0;\n\n float angle = 2.0 * 3.14159 * (index / segmentCount);\n\n vec3 u = getOrthogonalVector(d);\n vec3 v = normalize(cross(u, d));\n\n vec3 x = u * cos(angle) * length(d);\n vec3 y = v * sin(angle) * length(d);\n vec3 v3 = x + y;\n\n normal = normalize(v3);\n\n return v3;\n}\n\nattribute vec4 vector;\nattribute vec4 position;\nattribute vec4 id;\n\nuniform mat4 model, view, projection;\nuniform float tubeScale;\n\nvarying vec3 f_position;\nvarying vec4 f_id;\n\nvoid main() {\n vec3 normal;\n vec3 XYZ = getTubePosition(mat3(model) * (tubeScale * vector.w * normalize(vector.xyz)), position.w, normal);\n vec4 tubePosition = model * vec4(position.xyz, 1.0) + vec4(XYZ, 0.0);\n\n gl_Position = projection * view * tubePosition;\n f_id = id;\n f_position = position.xyz;\n}\n"]),s=n(["precision highp float;\n#define GLSLIFY 1\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nuniform vec3 clipBounds[2];\nuniform float pickId;\n\nvarying vec3 f_position;\nvarying vec4 f_id;\n\nvoid main() {\n if (outOfRange(clipBounds[0], clipBounds[1], f_position)) discard;\n\n gl_FragColor = vec4(pickId, f_id.xyz);\n}"]);r.meshShader={vertex:i,fragment:a,attributes:[{name:"position",type:"vec4"},{name:"color",type:"vec4"},{name:"uv",type:"vec2"},{name:"vector",type:"vec4"}]},r.pickShader={vertex:o,fragment:s,attributes:[{name:"position",type:"vec4"},{name:"id",type:"vec4"},{name:"vector",type:"vec4"}]}},{glslify:231}],143:[function(t,e,r){"use strict";var n=t("gl-vec3"),i=t("gl-vec4"),a=["xyz","xzy","yxz","yzx","zxy","zyx"],o=function(t,e,r,a){for(var o=0,s=0;s0)for(T=0;T<8;T++){var k=(T+1)%8;c.push(h[T],p[T],p[k],p[k],h[k],h[T]),f.push(y,v,v,v,y,y),d.push(m,g,g,g,m,m);var A=c.length;u.push([A-6,A-5,A-4],[A-3,A-2,A-1])}var M=h;h=p,p=M;var S=y;y=v,v=S;var E=m;m=g,g=E}return{positions:c,cells:u,vectors:f,vertexIntensity:d}}(t,r,a,o)})),f=[],h=[],p=[],d=[];for(s=0;se)return r-1}return r},l=function(t,e,r){return tr?r:t},c=function(t){var e=1/0;t.sort((function(t,e){return t-e}));for(var r=t.length,n=1;nf-1||y>h-1||x>p-1)return n.create();var b,_,w,T,k,A,M=a[0][d],S=a[0][v],E=a[1][m],L=a[1][y],C=a[2][g],P=(o-M)/(S-M),I=(c-E)/(L-E),O=(u-C)/(a[2][x]-C);switch(isFinite(P)||(P=.5),isFinite(I)||(I=.5),isFinite(O)||(O=.5),r.reversedX&&(d=f-1-d,v=f-1-v),r.reversedY&&(m=h-1-m,y=h-1-y),r.reversedZ&&(g=p-1-g,x=p-1-x),r.filled){case 5:k=g,A=x,w=m*p,T=y*p,b=d*p*h,_=v*p*h;break;case 4:k=g,A=x,b=d*p,_=v*p,w=m*p*f,T=y*p*f;break;case 3:w=m,T=y,k=g*h,A=x*h,b=d*h*p,_=v*h*p;break;case 2:w=m,T=y,b=d*h,_=v*h,k=g*h*f,A=x*h*f;break;case 1:b=d,_=v,k=g*f,A=x*f,w=m*f*p,T=y*f*p;break;default:b=d,_=v,w=m*f,T=y*f,k=g*f*h,A=x*f*h}var z=i[b+w+k],D=i[b+w+A],R=i[b+T+k],F=i[b+T+A],B=i[_+w+k],N=i[_+w+A],j=i[_+T+k],U=i[_+T+A],V=n.create(),H=n.create(),q=n.create(),G=n.create();n.lerp(V,z,B,P),n.lerp(H,D,N,P),n.lerp(q,R,j,P),n.lerp(G,F,U,P);var Y=n.create(),W=n.create();n.lerp(Y,V,q,I),n.lerp(W,H,G,I);var X=n.create();return n.lerp(X,Y,W,O),X}(e,t,p)},m=t.getDivergence||function(t,e){var r=n.create(),i=1e-4;n.add(r,t,[i,0,0]);var a=d(r);n.subtract(a,a,e),n.scale(a,a,1/i),n.add(r,t,[0,i,0]);var o=d(r);n.subtract(o,o,e),n.scale(o,o,1/i),n.add(r,t,[0,0,i]);var s=d(r);return n.subtract(s,s,e),n.scale(s,s,1/i),n.add(r,a,o),n.add(r,r,s),r},g=[],v=e[0][0],y=e[0][1],x=e[0][2],b=e[1][0],_=e[1][1],w=e[1][2],T=function(t){var e=t[0],r=t[1],n=t[2];return!(eb||r_||nw)},k=10*n.distance(e[0],e[1])/i,A=k*k,M=1,S=0,E=r.length;E>1&&(M=function(t){for(var e=[],r=[],n=[],i={},a={},o={},s=t.length,l=0;lS&&(S=F),D.push(F),g.push({points:P,velocities:I,divergences:D});for(var B=0;B<100*i&&P.lengthA&&n.scale(N,N,k/Math.sqrt(j)),n.add(N,N,C),O=d(N),n.squaredDistance(z,N)-A>-1e-4*A){P.push(N),z=N,I.push(O);R=m(N,O),F=n.length(R);isFinite(F)&&F>S&&(S=F),D.push(F)}C=N}}var U=o(g,t.colormap,S,M);return f?U.tubeScale=f:(0===S&&(S=1),U.tubeScale=.5*u*M/S),U};var u=t("./lib/shaders"),f=t("gl-cone3d").createMesh;e.exports.createTubeMesh=function(t,e){return f(t,e,{shaders:u,traceType:"streamtube"})}},{"./lib/shaders":142,"gl-cone3d":79,"gl-vec3":169,"gl-vec4":205}],144:[function(t,e,r){var n=t("gl-shader"),i=t("glslify"),a=i(["precision highp float;\n#define GLSLIFY 1\n\nattribute vec4 uv;\nattribute vec3 f;\nattribute vec3 normal;\n\nuniform vec3 objectOffset;\nuniform mat4 model, view, projection, inverseModel;\nuniform vec3 lightPosition, eyePosition;\nuniform sampler2D colormap;\n\nvarying float value, kill;\nvarying vec3 worldCoordinate;\nvarying vec2 planeCoordinate;\nvarying vec3 lightDirection, eyeDirection, surfaceNormal;\nvarying vec4 vColor;\n\nvoid main() {\n vec3 localCoordinate = vec3(uv.zw, f.x);\n worldCoordinate = objectOffset + localCoordinate;\n vec4 worldPosition = model * vec4(worldCoordinate, 1.0);\n vec4 clipPosition = projection * view * worldPosition;\n gl_Position = clipPosition;\n kill = f.y;\n value = f.z;\n planeCoordinate = uv.xy;\n\n vColor = texture2D(colormap, vec2(value, value));\n\n //Lighting geometry parameters\n vec4 cameraCoordinate = view * worldPosition;\n cameraCoordinate.xyz /= cameraCoordinate.w;\n lightDirection = lightPosition - cameraCoordinate.xyz;\n eyeDirection = eyePosition - cameraCoordinate.xyz;\n surfaceNormal = normalize((vec4(normal,0) * inverseModel).xyz);\n}\n"]),o=i(["precision highp float;\n#define GLSLIFY 1\n\nfloat beckmannDistribution(float x, float roughness) {\n float NdotH = max(x, 0.0001);\n float cos2Alpha = NdotH * NdotH;\n float tan2Alpha = (cos2Alpha - 1.0) / cos2Alpha;\n float roughness2 = roughness * roughness;\n float denom = 3.141592653589793 * roughness2 * cos2Alpha * cos2Alpha;\n return exp(tan2Alpha / roughness2) / denom;\n}\n\nfloat beckmannSpecular(\n vec3 lightDirection,\n vec3 viewDirection,\n vec3 surfaceNormal,\n float roughness) {\n return beckmannDistribution(dot(surfaceNormal, normalize(lightDirection + viewDirection)), roughness);\n}\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nuniform vec3 lowerBound, upperBound;\nuniform float contourTint;\nuniform vec4 contourColor;\nuniform sampler2D colormap;\nuniform vec3 clipBounds[2];\nuniform float roughness, fresnel, kambient, kdiffuse, kspecular, opacity;\nuniform float vertexColor;\n\nvarying float value, kill;\nvarying vec3 worldCoordinate;\nvarying vec3 lightDirection, eyeDirection, surfaceNormal;\nvarying vec4 vColor;\n\nvoid main() {\n if (\n kill > 0.0 ||\n vColor.a == 0.0 ||\n outOfRange(clipBounds[0], clipBounds[1], worldCoordinate)\n ) discard;\n\n vec3 N = normalize(surfaceNormal);\n vec3 V = normalize(eyeDirection);\n vec3 L = normalize(lightDirection);\n\n if(gl_FrontFacing) {\n N = -N;\n }\n\n float specular = max(beckmannSpecular(L, V, N, roughness), 0.);\n float diffuse = min(kambient + kdiffuse * max(dot(N, L), 0.0), 1.0);\n\n //decide how to interpolate color \u2014 in vertex or in fragment\n vec4 surfaceColor =\n step(vertexColor, .5) * texture2D(colormap, vec2(value, value)) +\n step(.5, vertexColor) * vColor;\n\n vec4 litColor = surfaceColor.a * vec4(diffuse * surfaceColor.rgb + kspecular * vec3(1,1,1) * specular, 1.0);\n\n gl_FragColor = mix(litColor, contourColor, contourTint) * opacity;\n}\n"]),s=i(["precision highp float;\n#define GLSLIFY 1\n\nattribute vec4 uv;\nattribute float f;\n\nuniform vec3 objectOffset;\nuniform mat3 permutation;\nuniform mat4 model, view, projection;\nuniform float height, zOffset;\nuniform sampler2D colormap;\n\nvarying float value, kill;\nvarying vec3 worldCoordinate;\nvarying vec2 planeCoordinate;\nvarying vec3 lightDirection, eyeDirection, surfaceNormal;\nvarying vec4 vColor;\n\nvoid main() {\n vec3 dataCoordinate = permutation * vec3(uv.xy, height);\n worldCoordinate = objectOffset + dataCoordinate;\n vec4 worldPosition = model * vec4(worldCoordinate, 1.0);\n\n vec4 clipPosition = projection * view * worldPosition;\n clipPosition.z += zOffset;\n\n gl_Position = clipPosition;\n value = f + objectOffset.z;\n kill = -1.0;\n planeCoordinate = uv.zw;\n\n vColor = texture2D(colormap, vec2(value, value));\n\n //Don't do lighting for contours\n surfaceNormal = vec3(1,0,0);\n eyeDirection = vec3(0,1,0);\n lightDirection = vec3(0,0,1);\n}\n"]),l=i(["precision highp float;\n#define GLSLIFY 1\n\nbool outOfRange(float a, float b, float p) {\n return ((p > max(a, b)) || \n (p < min(a, b)));\n}\n\nbool outOfRange(vec2 a, vec2 b, vec2 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y));\n}\n\nbool outOfRange(vec3 a, vec3 b, vec3 p) {\n return (outOfRange(a.x, b.x, p.x) ||\n outOfRange(a.y, b.y, p.y) ||\n outOfRange(a.z, b.z, p.z));\n}\n\nbool outOfRange(vec4 a, vec4 b, vec4 p) {\n return outOfRange(a.xyz, b.xyz, p.xyz);\n}\n\nuniform vec2 shape;\nuniform vec3 clipBounds[2];\nuniform float pickId;\n\nvarying float value, kill;\nvarying vec3 worldCoordinate;\nvarying vec2 planeCoordinate;\nvarying vec3 surfaceNormal;\n\nvec2 splitFloat(float v) {\n float vh = 255.0 * v;\n float upper = floor(vh);\n float lower = fract(vh);\n return vec2(upper / 255.0, floor(lower * 16.0) / 16.0);\n}\n\nvoid main() {\n if ((kill > 0.0) ||\n (outOfRange(clipBounds[0], clipBounds[1], worldCoordinate))) discard;\n\n vec2 ux = splitFloat(planeCoordinate.x / shape.x);\n vec2 uy = splitFloat(planeCoordinate.y / shape.y);\n gl_FragColor = vec4(pickId, ux.x, uy.x, ux.y + (uy.y/16.0));\n}\n"]);r.createShader=function(t){var e=n(t,a,o,null,[{name:"uv",type:"vec4"},{name:"f",type:"vec3"},{name:"normal",type:"vec3"}]);return e.attributes.uv.location=0,e.attributes.f.location=1,e.attributes.normal.location=2,e},r.createPickShader=function(t){var e=n(t,a,l,null,[{name:"uv",type:"vec4"},{name:"f",type:"vec3"},{name:"normal",type:"vec3"}]);return e.attributes.uv.location=0,e.attributes.f.location=1,e.attributes.normal.location=2,e},r.createContourShader=function(t){var e=n(t,s,o,null,[{name:"uv",type:"vec4"},{name:"f",type:"float"}]);return e.attributes.uv.location=0,e.attributes.f.location=1,e},r.createPickContourShader=function(t){var e=n(t,s,l,null,[{name:"uv",type:"vec4"},{name:"f",type:"float"}]);return e.attributes.uv.location=0,e.attributes.f.location=1,e}},{"gl-shader":132,glslify:231}],145:[function(t,e,r){"use strict";e.exports=function(t){var e=t.gl,r=y(e),n=b(e),s=x(e),l=_(e),c=i(e),u=a(e,[{buffer:c,size:4,stride:40,offset:0},{buffer:c,size:3,stride:40,offset:16},{buffer:c,size:3,stride:40,offset:28}]),f=i(e),h=a(e,[{buffer:f,size:4,stride:20,offset:0},{buffer:f,size:1,stride:20,offset:16}]),p=i(e),d=a(e,[{buffer:p,size:2,type:e.FLOAT}]),m=o(e,1,256,e.RGBA,e.UNSIGNED_BYTE);m.minFilter=e.LINEAR,m.magFilter=e.LINEAR;var g=new M(e,[0,0],[[0,0,0],[0,0,0]],r,n,c,u,m,s,l,f,h,p,d,[0,0,0]),v={levels:[[],[],[]]};for(var w in t)v[w]=t[w];return v.colormap=v.colormap||"jet",g.update(v),g};var n=t("bit-twiddle"),i=t("gl-buffer"),a=t("gl-vao"),o=t("gl-texture2d"),s=t("typedarray-pool"),l=t("colormap"),c=t("ndarray-ops"),u=t("ndarray-pack"),f=t("ndarray"),h=t("surface-nets"),p=t("gl-mat4/multiply"),d=t("gl-mat4/invert"),m=t("binary-search-bounds"),g=t("ndarray-gradient"),v=t("./lib/shaders"),y=v.createShader,x=v.createContourShader,b=v.createPickShader,_=v.createPickContourShader,w=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],T=[[0,0],[0,1],[1,0],[1,1],[1,0],[0,1]],k=[[0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0],[0,0,0,0,0,0,0,0,0]];function A(t,e,r,n,i){this.position=t,this.index=e,this.uv=r,this.level=n,this.dataCoordinate=i}!function(){for(var t=0;t<3;++t){var e=k[t],r=(t+2)%3;e[(t+1)%3+0]=1,e[r+3]=1,e[t+6]=1}}();function M(t,e,r,n,i,a,o,l,c,u,h,p,d,m,g){this.gl=t,this.shape=e,this.bounds=r,this.objectOffset=g,this.intensityBounds=[],this._shader=n,this._pickShader=i,this._coordinateBuffer=a,this._vao=o,this._colorMap=l,this._contourShader=c,this._contourPickShader=u,this._contourBuffer=h,this._contourVAO=p,this._contourOffsets=[[],[],[]],this._contourCounts=[[],[],[]],this._vertexCount=0,this._pickResult=new A([0,0,0],[0,0],[0,0],[0,0,0],[0,0,0]),this._dynamicBuffer=d,this._dynamicVAO=m,this._dynamicOffsets=[0,0,0],this._dynamicCounts=[0,0,0],this.contourWidth=[1,1,1],this.contourLevels=[[1],[1],[1]],this.contourTint=[0,0,0],this.contourColor=[[.5,.5,.5,1],[.5,.5,.5,1],[.5,.5,.5,1]],this.showContour=!0,this.showSurface=!0,this.enableHighlight=[!0,!0,!0],this.highlightColor=[[0,0,0,1],[0,0,0,1],[0,0,0,1]],this.highlightTint=[1,1,1],this.highlightLevel=[-1,-1,-1],this.enableDynamic=[!0,!0,!0],this.dynamicLevel=[NaN,NaN,NaN],this.dynamicColor=[[0,0,0,1],[0,0,0,1],[0,0,0,1]],this.dynamicTint=[1,1,1],this.dynamicWidth=[1,1,1],this.axesBounds=[[1/0,1/0,1/0],[-1/0,-1/0,-1/0]],this.surfaceProject=[!1,!1,!1],this.contourProject=[[!1,!1,!1],[!1,!1,!1],[!1,!1,!1]],this.colorBounds=[!1,!1],this._field=[f(s.mallocFloat(1024),[0,0]),f(s.mallocFloat(1024),[0,0]),f(s.mallocFloat(1024),[0,0])],this.pickId=1,this.clipBounds=[[-1/0,-1/0,-1/0],[1/0,1/0,1/0]],this.snapToData=!1,this.pixelRatio=1,this.opacity=1,this.lightPosition=[10,1e4,0],this.ambientLight=.8,this.diffuseLight=.8,this.specularLight=2,this.roughness=.5,this.fresnel=1.5,this.vertexColor=0,this.dirty=!0}var S=M.prototype;S.genColormap=function(t,e){var r=!1,n=u([l({colormap:t,nshades:256,format:"rgba"}).map((function(t,n){var i=e?function(t,e){if(!e)return 1;if(!e.length)return 1;for(var r=0;rt&&r>0){var n=(e[r][0]-t)/(e[r][0]-e[r-1][0]);return e[r][1]*(1-n)+n*e[r-1][1]}}return 1}(n/255,e):t[3];return i<1&&(r=!0),[t[0],t[1],t[2],255*i]}))]);return c.divseq(n,255),this.hasAlphaScale=r,n},S.isTransparent=function(){return this.opacity<1||this.hasAlphaScale},S.isOpaque=function(){return!this.isTransparent()},S.pickSlots=1,S.setPickBase=function(t){this.pickId=t};var E=[0,0,0],L={showSurface:!1,showContour:!1,projections:[w.slice(),w.slice(),w.slice()],clipBounds:[[[0,0,0],[0,0,0]],[[0,0,0],[0,0,0]],[[0,0,0],[0,0,0]]]};function C(t,e){var r,n,i,a=e.axes&&e.axes.lastCubeProps.axis||E,o=e.showSurface,s=e.showContour;for(r=0;r<3;++r)for(o=o||e.surfaceProject[r],n=0;n<3;++n)s=s||e.contourProject[r][n];for(r=0;r<3;++r){var l=L.projections[r];for(n=0;n<16;++n)l[n]=0;for(n=0;n<4;++n)l[5*n]=1;l[5*r]=0,l[12+r]=e.axesBounds[+(a[r]>0)][r],p(l,t.model,l);var c=L.clipBounds[r];for(i=0;i<2;++i)for(n=0;n<3;++n)c[i][n]=t.clipBounds[i][n];c[0][r]=-1e8,c[1][r]=1e8}return L.showSurface=o,L.showContour=s,L}var P={model:w,view:w,projection:w,inverseModel:w.slice(),lowerBound:[0,0,0],upperBound:[0,0,0],colorMap:0,clipBounds:[[0,0,0],[0,0,0]],height:0,contourTint:0,contourColor:[0,0,0,1],permutation:[1,0,0,0,1,0,0,0,1],zOffset:-1e-4,objectOffset:[0,0,0],kambient:1,kdiffuse:1,kspecular:1,lightPosition:[1e3,1e3,1e3],eyePosition:[0,0,0],roughness:1,fresnel:1,opacity:1,vertexColor:0},I=w.slice(),O=[1,0,0,0,1,0,0,0,1];function z(t,e){t=t||{};var r=this.gl;r.disable(r.CULL_FACE),this._colorMap.bind(0);var n=P;n.model=t.model||w,n.view=t.view||w,n.projection=t.projection||w,n.lowerBound=[this.bounds[0][0],this.bounds[0][1],this.colorBounds[0]||this.bounds[0][2]],n.upperBound=[this.bounds[1][0],this.bounds[1][1],this.colorBounds[1]||this.bounds[1][2]],n.objectOffset=this.objectOffset,n.contourColor=this.contourColor[0],n.inverseModel=d(n.inverseModel,n.model);for(var i=0;i<2;++i)for(var a=n.clipBounds[i],o=0;o<3;++o)a[o]=Math.min(Math.max(this.clipBounds[i][o],-1e8),1e8);n.kambient=this.ambientLight,n.kdiffuse=this.diffuseLight,n.kspecular=this.specularLight,n.roughness=this.roughness,n.fresnel=this.fresnel,n.opacity=this.opacity,n.height=0,n.permutation=O,n.vertexColor=this.vertexColor;var s=I;for(p(s,n.view,n.model),p(s,n.projection,s),d(s,s),i=0;i<3;++i)n.eyePosition[i]=s[12+i]/s[15];var l=s[15];for(i=0;i<3;++i)l+=this.lightPosition[i]*s[4*i+3];for(i=0;i<3;++i){var c=s[12+i];for(o=0;o<3;++o)c+=s[4*o+i]*this.lightPosition[o];n.lightPosition[i]=c/l}var u=C(n,this);if(u.showSurface){for(this._shader.bind(),this._shader.uniforms=n,this._vao.bind(),this.showSurface&&this._vertexCount&&this._vao.draw(r.TRIANGLES,this._vertexCount),i=0;i<3;++i)this.surfaceProject[i]&&this.vertexCount&&(this._shader.uniforms.model=u.projections[i],this._shader.uniforms.clipBounds=u.clipBounds[i],this._vao.draw(r.TRIANGLES,this._vertexCount));this._vao.unbind()}if(u.showContour){var f=this._contourShader;n.kambient=1,n.kdiffuse=0,n.kspecular=0,n.opacity=1,f.bind(),f.uniforms=n;var h=this._contourVAO;for(h.bind(),i=0;i<3;++i)for(f.uniforms.permutation=k[i],r.lineWidth(this.contourWidth[i]*this.pixelRatio),o=0;o>4)/16)/255,i=Math.floor(n),a=n-i,o=e[1]*(t.value[1]+(15&t.value[2])/16)/255,s=Math.floor(o),l=o-s;i+=1,s+=1;var c=r.position;c[0]=c[1]=c[2]=0;for(var u=0;u<2;++u)for(var f=u?a:1-a,h=0;h<2;++h)for(var p=i+u,d=s+h,g=f*(h?l:1-l),v=0;v<3;++v)c[v]+=this._field[v].get(p,d)*g;for(var y=this._pickResult.level,x=0;x<3;++x)if(y[x]=m.le(this.contourLevels[x],c[x]),y[x]<0)this.contourLevels[x].length>0&&(y[x]=0);else if(y[x]Math.abs(_-c[x])&&(y[x]+=1)}for(r.index[0]=a<.5?i:i+1,r.index[1]=l<.5?s:s+1,r.uv[0]=n/e[0],r.uv[1]=o/e[1],v=0;v<3;++v)r.dataCoordinate[v]=this._field[v].get(r.index[0],r.index[1]);return r},S.padField=function(t,e){var r=e.shape.slice(),n=t.shape.slice();c.assign(t.lo(1,1).hi(r[0],r[1]),e),c.assign(t.lo(1).hi(r[0],1),e.hi(r[0],1)),c.assign(t.lo(1,n[1]-1).hi(r[0],1),e.lo(0,r[1]-1).hi(r[0],1)),c.assign(t.lo(0,1).hi(1,r[1]),e.hi(1)),c.assign(t.lo(n[0]-1,1).hi(1,r[1]),e.lo(r[0]-1)),t.set(0,0,e.get(0,0)),t.set(0,n[1]-1,e.get(0,r[1]-1)),t.set(n[0]-1,0,e.get(r[0]-1,0)),t.set(n[0]-1,n[1]-1,e.get(r[0]-1,r[1]-1))},S.update=function(t){t=t||{},this.objectOffset=t.objectOffset||this.objectOffset,this.dirty=!0,"contourWidth"in t&&(this.contourWidth=R(t.contourWidth,Number)),"showContour"in t&&(this.showContour=R(t.showContour,Boolean)),"showSurface"in t&&(this.showSurface=!!t.showSurface),"contourTint"in t&&(this.contourTint=R(t.contourTint,Boolean)),"contourColor"in t&&(this.contourColor=B(t.contourColor)),"contourProject"in t&&(this.contourProject=R(t.contourProject,(function(t){return R(t,Boolean)}))),"surfaceProject"in t&&(this.surfaceProject=t.surfaceProject),"dynamicColor"in t&&(this.dynamicColor=B(t.dynamicColor)),"dynamicTint"in t&&(this.dynamicTint=R(t.dynamicTint,Number)),"dynamicWidth"in t&&(this.dynamicWidth=R(t.dynamicWidth,Number)),"opacity"in t&&(this.opacity=t.opacity),"opacityscale"in t&&(this.opacityscale=t.opacityscale),"colorBounds"in t&&(this.colorBounds=t.colorBounds),"vertexColor"in t&&(this.vertexColor=t.vertexColor?1:0),"colormap"in t&&this._colorMap.setPixels(this.genColormap(t.colormap,this.opacityscale));var e=t.field||t.coords&&t.coords[2]||null,r=!1;if(e||(e=this._field[2].shape[0]||this._field[2].shape[2]?this._field[2].lo(1,1).hi(this._field[2].shape[0]-2,this._field[2].shape[1]-2):this._field[2].hi(0,0)),"field"in t||"coords"in t){var i=(e.shape[0]+2)*(e.shape[1]+2);i>this._field[2].data.length&&(s.freeFloat(this._field[2].data),this._field[2].data=s.mallocFloat(n.nextPow2(i))),this._field[2]=f(this._field[2].data,[e.shape[0]+2,e.shape[1]+2]),this.padField(this._field[2],e),this.shape=e.shape.slice();for(var a=this.shape,o=0;o<2;++o)this._field[2].size>this._field[o].data.length&&(s.freeFloat(this._field[o].data),this._field[o].data=s.mallocFloat(this._field[2].size)),this._field[o]=f(this._field[o].data,[a[0]+2,a[1]+2]);if(t.coords){var l=t.coords;if(!Array.isArray(l)||3!==l.length)throw new Error("gl-surface: invalid coordinates for x/y");for(o=0;o<2;++o){var c=l[o];for(v=0;v<2;++v)if(c.shape[v]!==a[v])throw new Error("gl-surface: coords have incorrect shape");this.padField(this._field[o],c)}}else if(t.ticks){var u=t.ticks;if(!Array.isArray(u)||2!==u.length)throw new Error("gl-surface: invalid ticks");for(o=0;o<2;++o){var p=u[o];if((Array.isArray(p)||p.length)&&(p=f(p)),p.shape[0]!==a[o])throw new Error("gl-surface: invalid tick length");var d=f(p.data,a);d.stride[o]=p.stride[0],d.stride[1^o]=0,this.padField(this._field[o],d)}}else{for(o=0;o<2;++o){var m=[0,0];m[o]=1,this._field[o]=f(this._field[o].data,[a[0]+2,a[1]+2],m,0)}this._field[0].set(0,0,0);for(var v=0;v0){for(var xt=0;xt<5;++xt)Q.pop();U-=1}continue t}Q.push(nt[0],nt[1],ot[0],ot[1],nt[2]),U+=1}}rt.push(U)}this._contourOffsets[$]=et,this._contourCounts[$]=rt}var bt=s.mallocFloat(Q.length);for(o=0;oi||r<0||r>i)throw new Error("gl-texture2d: Invalid texture size");return t._shape=[e,r],t.bind(),n.texImage2D(n.TEXTURE_2D,0,t.format,e,r,0,t.format,t.type,null),t._mipLevels=[0],t}function p(t,e,r,n,i,a){this.gl=t,this.handle=e,this.format=i,this.type=a,this._shape=[r,n],this._mipLevels=[0],this._magFilter=t.NEAREST,this._minFilter=t.NEAREST,this._wrapS=t.CLAMP_TO_EDGE,this._wrapT=t.CLAMP_TO_EDGE,this._anisoSamples=1;var o=this,s=[this._wrapS,this._wrapT];Object.defineProperties(s,[{get:function(){return o._wrapS},set:function(t){return o.wrapS=t}},{get:function(){return o._wrapT},set:function(t){return o.wrapT=t}}]),this._wrapVector=s;var l=[this._shape[0],this._shape[1]];Object.defineProperties(l,[{get:function(){return o._shape[0]},set:function(t){return o.width=t}},{get:function(){return o._shape[1]},set:function(t){return o.height=t}}]),this._shapeVector=l}var d=p.prototype;function m(t,e){return 3===t.length?1===e[2]&&e[1]===t[0]*t[2]&&e[0]===t[2]:1===e[0]&&e[1]===t[0]}function g(t){var e=t.createTexture();return t.bindTexture(t.TEXTURE_2D,e),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MIN_FILTER,t.NEAREST),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MAG_FILTER,t.NEAREST),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_S,t.CLAMP_TO_EDGE),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_T,t.CLAMP_TO_EDGE),e}function v(t,e,r,n,i){var a=t.getParameter(t.MAX_TEXTURE_SIZE);if(e<0||e>a||r<0||r>a)throw new Error("gl-texture2d: Invalid texture shape");if(i===t.FLOAT&&!t.getExtension("OES_texture_float"))throw new Error("gl-texture2d: Floating point textures not supported on this platform");var o=g(t);return t.texImage2D(t.TEXTURE_2D,0,n,e,r,0,n,i,null),new p(t,o,e,r,n,i)}function y(t,e,r,n,i,a){var o=g(t);return t.texImage2D(t.TEXTURE_2D,0,i,i,a,e),new p(t,o,r,n,i,a)}function x(t,e){var r=e.dtype,o=e.shape.slice(),s=t.getParameter(t.MAX_TEXTURE_SIZE);if(o[0]<0||o[0]>s||o[1]<0||o[1]>s)throw new Error("gl-texture2d: Invalid texture size");var l=m(o,e.stride.slice()),c=0;"float32"===r?c=t.FLOAT:"float64"===r?(c=t.FLOAT,l=!1,r="float32"):"uint8"===r?c=t.UNSIGNED_BYTE:(c=t.UNSIGNED_BYTE,l=!1,r="uint8");var u,h,d=0;if(2===o.length)d=t.LUMINANCE,o=[o[0],o[1],1],e=n(e.data,o,[e.stride[0],e.stride[1],1],e.offset);else{if(3!==o.length)throw new Error("gl-texture2d: Invalid shape for texture");if(1===o[2])d=t.ALPHA;else if(2===o[2])d=t.LUMINANCE_ALPHA;else if(3===o[2])d=t.RGB;else{if(4!==o[2])throw new Error("gl-texture2d: Invalid shape for pixel coords");d=t.RGBA}}c!==t.FLOAT||t.getExtension("OES_texture_float")||(c=t.UNSIGNED_BYTE,l=!1);var v=e.size;if(l)u=0===e.offset&&e.data.length===v?e.data:e.data.subarray(e.offset,e.offset+v);else{var y=[o[2],o[2]*o[0],1];h=a.malloc(v,r);var x=n(h,o,y,0);"float32"!==r&&"float64"!==r||c!==t.UNSIGNED_BYTE?i.assign(x,e):f(x,e),u=h.subarray(0,v)}var b=g(t);return t.texImage2D(t.TEXTURE_2D,0,d,o[0],o[1],0,d,c,u),l||a.free(h),new p(t,b,o[0],o[1],d,c)}Object.defineProperties(d,{minFilter:{get:function(){return this._minFilter},set:function(t){this.bind();var e=this.gl;if(this.type===e.FLOAT&&o.indexOf(t)>=0&&(e.getExtension("OES_texture_float_linear")||(t=e.NEAREST)),s.indexOf(t)<0)throw new Error("gl-texture2d: Unknown filter mode "+t);return e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,t),this._minFilter=t}},magFilter:{get:function(){return this._magFilter},set:function(t){this.bind();var e=this.gl;if(this.type===e.FLOAT&&o.indexOf(t)>=0&&(e.getExtension("OES_texture_float_linear")||(t=e.NEAREST)),s.indexOf(t)<0)throw new Error("gl-texture2d: Unknown filter mode "+t);return e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,t),this._magFilter=t}},mipSamples:{get:function(){return this._anisoSamples},set:function(t){var e=this._anisoSamples;if(this._anisoSamples=0|Math.max(t,1),e!==this._anisoSamples){var r=this.gl.getExtension("EXT_texture_filter_anisotropic");r&&this.gl.texParameterf(this.gl.TEXTURE_2D,r.TEXTURE_MAX_ANISOTROPY_EXT,this._anisoSamples)}return this._anisoSamples}},wrapS:{get:function(){return this._wrapS},set:function(t){if(this.bind(),l.indexOf(t)<0)throw new Error("gl-texture2d: Unknown wrap mode "+t);return this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_S,t),this._wrapS=t}},wrapT:{get:function(){return this._wrapT},set:function(t){if(this.bind(),l.indexOf(t)<0)throw new Error("gl-texture2d: Unknown wrap mode "+t);return this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_T,t),this._wrapT=t}},wrap:{get:function(){return this._wrapVector},set:function(t){if(Array.isArray(t)||(t=[t,t]),2!==t.length)throw new Error("gl-texture2d: Must specify wrap mode for rows and columns");for(var e=0;e<2;++e)if(l.indexOf(t[e])<0)throw new Error("gl-texture2d: Unknown wrap mode "+t);this._wrapS=t[0],this._wrapT=t[1];var r=this.gl;return this.bind(),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_S,this._wrapS),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_T,this._wrapT),t}},shape:{get:function(){return this._shapeVector},set:function(t){if(Array.isArray(t)){if(2!==t.length)throw new Error("gl-texture2d: Invalid texture shape")}else t=[0|t,0|t];return h(this,0|t[0],0|t[1]),[0|t[0],0|t[1]]}},width:{get:function(){return this._shape[0]},set:function(t){return h(this,t|=0,this._shape[1]),t}},height:{get:function(){return this._shape[1]},set:function(t){return t|=0,h(this,this._shape[0],t),t}}}),d.bind=function(t){var e=this.gl;return void 0!==t&&e.activeTexture(e.TEXTURE0+(0|t)),e.bindTexture(e.TEXTURE_2D,this.handle),void 0!==t?0|t:e.getParameter(e.ACTIVE_TEXTURE)-e.TEXTURE0},d.dispose=function(){this.gl.deleteTexture(this.handle)},d.generateMipmap=function(){this.bind(),this.gl.generateMipmap(this.gl.TEXTURE_2D);for(var t=Math.min(this._shape[0],this._shape[1]),e=0;t>0;++e,t>>>=1)this._mipLevels.indexOf(e)<0&&this._mipLevels.push(e)},d.setPixels=function(t,e,r,o){var s=this.gl;this.bind(),Array.isArray(e)?(o=r,r=0|e[1],e=0|e[0]):(e=e||0,r=r||0),o=o||0;var l=u(t)?t:t.raw;if(l){this._mipLevels.indexOf(o)<0?(s.texImage2D(s.TEXTURE_2D,0,this.format,this.format,this.type,l),this._mipLevels.push(o)):s.texSubImage2D(s.TEXTURE_2D,o,e,r,this.format,this.type,l)}else{if(!(t.shape&&t.stride&&t.data))throw new Error("gl-texture2d: Unsupported data type");if(t.shape.length<2||e+t.shape[1]>this._shape[1]>>>o||r+t.shape[0]>this._shape[0]>>>o||e<0||r<0)throw new Error("gl-texture2d: Texture dimensions are out of bounds");!function(t,e,r,o,s,l,c,u){var h=u.dtype,p=u.shape.slice();if(p.length<2||p.length>3)throw new Error("gl-texture2d: Invalid ndarray, must be 2d or 3d");var d=0,g=0,v=m(p,u.stride.slice());"float32"===h?d=t.FLOAT:"float64"===h?(d=t.FLOAT,v=!1,h="float32"):"uint8"===h?d=t.UNSIGNED_BYTE:(d=t.UNSIGNED_BYTE,v=!1,h="uint8");if(2===p.length)g=t.LUMINANCE,p=[p[0],p[1],1],u=n(u.data,p,[u.stride[0],u.stride[1],1],u.offset);else{if(3!==p.length)throw new Error("gl-texture2d: Invalid shape for texture");if(1===p[2])g=t.ALPHA;else if(2===p[2])g=t.LUMINANCE_ALPHA;else if(3===p[2])g=t.RGB;else{if(4!==p[2])throw new Error("gl-texture2d: Invalid shape for pixel coords");g=t.RGBA}p[2]}g!==t.LUMINANCE&&g!==t.ALPHA||s!==t.LUMINANCE&&s!==t.ALPHA||(g=s);if(g!==s)throw new Error("gl-texture2d: Incompatible texture format for setPixels");var y=u.size,x=c.indexOf(o)<0;x&&c.push(o);if(d===l&&v)0===u.offset&&u.data.length===y?x?t.texImage2D(t.TEXTURE_2D,o,s,p[0],p[1],0,s,l,u.data):t.texSubImage2D(t.TEXTURE_2D,o,e,r,p[0],p[1],s,l,u.data):x?t.texImage2D(t.TEXTURE_2D,o,s,p[0],p[1],0,s,l,u.data.subarray(u.offset,u.offset+y)):t.texSubImage2D(t.TEXTURE_2D,o,e,r,p[0],p[1],s,l,u.data.subarray(u.offset,u.offset+y));else{var b;b=l===t.FLOAT?a.mallocFloat32(y):a.mallocUint8(y);var _=n(b,p,[p[2],p[2]*p[0],1]);d===t.FLOAT&&l===t.UNSIGNED_BYTE?f(_,u):i.assign(_,u),x?t.texImage2D(t.TEXTURE_2D,o,s,p[0],p[1],0,s,l,b.subarray(0,y)):t.texSubImage2D(t.TEXTURE_2D,o,e,r,p[0],p[1],s,l,b.subarray(0,y)),l===t.FLOAT?a.freeFloat32(b):a.freeUint8(b)}}(s,e,r,o,this.format,this.type,this._mipLevels,t)}}},{ndarray:259,"ndarray-ops":254,"typedarray-pool":308}],147:[function(t,e,r){"use strict";e.exports=function(t,e,r){e?e.bind():t.bindBuffer(t.ELEMENT_ARRAY_BUFFER,null);var n=0|t.getParameter(t.MAX_VERTEX_ATTRIBS);if(r){if(r.length>n)throw new Error("gl-vao: Too many vertex attributes");for(var i=0;i1?0:Math.acos(s)};var n=t("./fromValues"),i=t("./normalize"),a=t("./dot")},{"./dot":162,"./fromValues":168,"./normalize":179}],153:[function(t,e,r){e.exports=function(t,e){return t[0]=Math.ceil(e[0]),t[1]=Math.ceil(e[1]),t[2]=Math.ceil(e[2]),t}},{}],154:[function(t,e,r){e.exports=function(t){var e=new Float32Array(3);return e[0]=t[0],e[1]=t[1],e[2]=t[2],e}},{}],155:[function(t,e,r){e.exports=function(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t}},{}],156:[function(t,e,r){e.exports=function(){var t=new Float32Array(3);return t[0]=0,t[1]=0,t[2]=0,t}},{}],157:[function(t,e,r){e.exports=function(t,e,r){var n=e[0],i=e[1],a=e[2],o=r[0],s=r[1],l=r[2];return t[0]=i*l-a*s,t[1]=a*o-n*l,t[2]=n*s-i*o,t}},{}],158:[function(t,e,r){e.exports=t("./distance")},{"./distance":159}],159:[function(t,e,r){e.exports=function(t,e){var r=e[0]-t[0],n=e[1]-t[1],i=e[2]-t[2];return Math.sqrt(r*r+n*n+i*i)}},{}],160:[function(t,e,r){e.exports=t("./divide")},{"./divide":161}],161:[function(t,e,r){e.exports=function(t,e,r){return t[0]=e[0]/r[0],t[1]=e[1]/r[1],t[2]=e[2]/r[2],t}},{}],162:[function(t,e,r){e.exports=function(t,e){return t[0]*e[0]+t[1]*e[1]+t[2]*e[2]}},{}],163:[function(t,e,r){e.exports=1e-6},{}],164:[function(t,e,r){e.exports=function(t,e){var r=t[0],i=t[1],a=t[2],o=e[0],s=e[1],l=e[2];return Math.abs(r-o)<=n*Math.max(1,Math.abs(r),Math.abs(o))&&Math.abs(i-s)<=n*Math.max(1,Math.abs(i),Math.abs(s))&&Math.abs(a-l)<=n*Math.max(1,Math.abs(a),Math.abs(l))};var n=t("./epsilon")},{"./epsilon":163}],165:[function(t,e,r){e.exports=function(t,e){return t[0]===e[0]&&t[1]===e[1]&&t[2]===e[2]}},{}],166:[function(t,e,r){e.exports=function(t,e){return t[0]=Math.floor(e[0]),t[1]=Math.floor(e[1]),t[2]=Math.floor(e[2]),t}},{}],167:[function(t,e,r){e.exports=function(t,e,r,i,a,o){var s,l;e||(e=3);r||(r=0);l=i?Math.min(i*e+r,t.length):t.length;for(s=r;s0&&(a=1/Math.sqrt(a),t[0]=e[0]*a,t[1]=e[1]*a,t[2]=e[2]*a);return t}},{}],180:[function(t,e,r){e.exports=function(t,e){e=e||1;var r=2*Math.random()*Math.PI,n=2*Math.random()-1,i=Math.sqrt(1-n*n)*e;return t[0]=Math.cos(r)*i,t[1]=Math.sin(r)*i,t[2]=n*e,t}},{}],181:[function(t,e,r){e.exports=function(t,e,r,n){var i=r[1],a=r[2],o=e[1]-i,s=e[2]-a,l=Math.sin(n),c=Math.cos(n);return t[0]=e[0],t[1]=i+o*c-s*l,t[2]=a+o*l+s*c,t}},{}],182:[function(t,e,r){e.exports=function(t,e,r,n){var i=r[0],a=r[2],o=e[0]-i,s=e[2]-a,l=Math.sin(n),c=Math.cos(n);return t[0]=i+s*l+o*c,t[1]=e[1],t[2]=a+s*c-o*l,t}},{}],183:[function(t,e,r){e.exports=function(t,e,r,n){var i=r[0],a=r[1],o=e[0]-i,s=e[1]-a,l=Math.sin(n),c=Math.cos(n);return t[0]=i+o*c-s*l,t[1]=a+o*l+s*c,t[2]=e[2],t}},{}],184:[function(t,e,r){e.exports=function(t,e){return t[0]=Math.round(e[0]),t[1]=Math.round(e[1]),t[2]=Math.round(e[2]),t}},{}],185:[function(t,e,r){e.exports=function(t,e,r){return t[0]=e[0]*r,t[1]=e[1]*r,t[2]=e[2]*r,t}},{}],186:[function(t,e,r){e.exports=function(t,e,r,n){return t[0]=e[0]+r[0]*n,t[1]=e[1]+r[1]*n,t[2]=e[2]+r[2]*n,t}},{}],187:[function(t,e,r){e.exports=function(t,e,r,n){return t[0]=e,t[1]=r,t[2]=n,t}},{}],188:[function(t,e,r){e.exports=t("./squaredDistance")},{"./squaredDistance":190}],189:[function(t,e,r){e.exports=t("./squaredLength")},{"./squaredLength":191}],190:[function(t,e,r){e.exports=function(t,e){var r=e[0]-t[0],n=e[1]-t[1],i=e[2]-t[2];return r*r+n*n+i*i}},{}],191:[function(t,e,r){e.exports=function(t){var e=t[0],r=t[1],n=t[2];return e*e+r*r+n*n}},{}],192:[function(t,e,r){e.exports=t("./subtract")},{"./subtract":193}],193:[function(t,e,r){e.exports=function(t,e,r){return t[0]=e[0]-r[0],t[1]=e[1]-r[1],t[2]=e[2]-r[2],t}},{}],194:[function(t,e,r){e.exports=function(t,e,r){var n=e[0],i=e[1],a=e[2];return t[0]=n*r[0]+i*r[3]+a*r[6],t[1]=n*r[1]+i*r[4]+a*r[7],t[2]=n*r[2]+i*r[5]+a*r[8],t}},{}],195:[function(t,e,r){e.exports=function(t,e,r){var n=e[0],i=e[1],a=e[2],o=r[3]*n+r[7]*i+r[11]*a+r[15];return o=o||1,t[0]=(r[0]*n+r[4]*i+r[8]*a+r[12])/o,t[1]=(r[1]*n+r[5]*i+r[9]*a+r[13])/o,t[2]=(r[2]*n+r[6]*i+r[10]*a+r[14])/o,t}},{}],196:[function(t,e,r){e.exports=function(t,e,r){var n=e[0],i=e[1],a=e[2],o=r[0],s=r[1],l=r[2],c=r[3],u=c*n+s*a-l*i,f=c*i+l*n-o*a,h=c*a+o*i-s*n,p=-o*n-s*i-l*a;return t[0]=u*c+p*-o+f*-l-h*-s,t[1]=f*c+p*-s+h*-o-u*-l,t[2]=h*c+p*-l+u*-s-f*-o,t}},{}],197:[function(t,e,r){e.exports=function(t,e,r){return t[0]=e[0]+r[0],t[1]=e[1]+r[1],t[2]=e[2]+r[2],t[3]=e[3]+r[3],t}},{}],198:[function(t,e,r){e.exports=function(t){var e=new Float32Array(4);return e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3],e}},{}],199:[function(t,e,r){e.exports=function(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t[3]=e[3],t}},{}],200:[function(t,e,r){e.exports=function(){var t=new Float32Array(4);return t[0]=0,t[1]=0,t[2]=0,t[3]=0,t}},{}],201:[function(t,e,r){e.exports=function(t,e){var r=e[0]-t[0],n=e[1]-t[1],i=e[2]-t[2],a=e[3]-t[3];return Math.sqrt(r*r+n*n+i*i+a*a)}},{}],202:[function(t,e,r){e.exports=function(t,e,r){return t[0]=e[0]/r[0],t[1]=e[1]/r[1],t[2]=e[2]/r[2],t[3]=e[3]/r[3],t}},{}],203:[function(t,e,r){e.exports=function(t,e){return t[0]*e[0]+t[1]*e[1]+t[2]*e[2]+t[3]*e[3]}},{}],204:[function(t,e,r){e.exports=function(t,e,r,n){var i=new Float32Array(4);return i[0]=t,i[1]=e,i[2]=r,i[3]=n,i}},{}],205:[function(t,e,r){e.exports={create:t("./create"),clone:t("./clone"),fromValues:t("./fromValues"),copy:t("./copy"),set:t("./set"),add:t("./add"),subtract:t("./subtract"),multiply:t("./multiply"),divide:t("./divide"),min:t("./min"),max:t("./max"),scale:t("./scale"),scaleAndAdd:t("./scaleAndAdd"),distance:t("./distance"),squaredDistance:t("./squaredDistance"),length:t("./length"),squaredLength:t("./squaredLength"),negate:t("./negate"),inverse:t("./inverse"),normalize:t("./normalize"),dot:t("./dot"),lerp:t("./lerp"),random:t("./random"),transformMat4:t("./transformMat4"),transformQuat:t("./transformQuat")}},{"./add":197,"./clone":198,"./copy":199,"./create":200,"./distance":201,"./divide":202,"./dot":203,"./fromValues":204,"./inverse":206,"./length":207,"./lerp":208,"./max":209,"./min":210,"./multiply":211,"./negate":212,"./normalize":213,"./random":214,"./scale":215,"./scaleAndAdd":216,"./set":217,"./squaredDistance":218,"./squaredLength":219,"./subtract":220,"./transformMat4":221,"./transformQuat":222}],206:[function(t,e,r){e.exports=function(t,e){return t[0]=1/e[0],t[1]=1/e[1],t[2]=1/e[2],t[3]=1/e[3],t}},{}],207:[function(t,e,r){e.exports=function(t){var e=t[0],r=t[1],n=t[2],i=t[3];return Math.sqrt(e*e+r*r+n*n+i*i)}},{}],208:[function(t,e,r){e.exports=function(t,e,r,n){var i=e[0],a=e[1],o=e[2],s=e[3];return t[0]=i+n*(r[0]-i),t[1]=a+n*(r[1]-a),t[2]=o+n*(r[2]-o),t[3]=s+n*(r[3]-s),t}},{}],209:[function(t,e,r){e.exports=function(t,e,r){return t[0]=Math.max(e[0],r[0]),t[1]=Math.max(e[1],r[1]),t[2]=Math.max(e[2],r[2]),t[3]=Math.max(e[3],r[3]),t}},{}],210:[function(t,e,r){e.exports=function(t,e,r){return t[0]=Math.min(e[0],r[0]),t[1]=Math.min(e[1],r[1]),t[2]=Math.min(e[2],r[2]),t[3]=Math.min(e[3],r[3]),t}},{}],211:[function(t,e,r){e.exports=function(t,e,r){return t[0]=e[0]*r[0],t[1]=e[1]*r[1],t[2]=e[2]*r[2],t[3]=e[3]*r[3],t}},{}],212:[function(t,e,r){e.exports=function(t,e){return t[0]=-e[0],t[1]=-e[1],t[2]=-e[2],t[3]=-e[3],t}},{}],213:[function(t,e,r){e.exports=function(t,e){var r=e[0],n=e[1],i=e[2],a=e[3],o=r*r+n*n+i*i+a*a;o>0&&(o=1/Math.sqrt(o),t[0]=r*o,t[1]=n*o,t[2]=i*o,t[3]=a*o);return t}},{}],214:[function(t,e,r){var n=t("./normalize"),i=t("./scale");e.exports=function(t,e){return e=e||1,t[0]=Math.random(),t[1]=Math.random(),t[2]=Math.random(),t[3]=Math.random(),n(t,t),i(t,t,e),t}},{"./normalize":213,"./scale":215}],215:[function(t,e,r){e.exports=function(t,e,r){return t[0]=e[0]*r,t[1]=e[1]*r,t[2]=e[2]*r,t[3]=e[3]*r,t}},{}],216:[function(t,e,r){e.exports=function(t,e,r,n){return t[0]=e[0]+r[0]*n,t[1]=e[1]+r[1]*n,t[2]=e[2]+r[2]*n,t[3]=e[3]+r[3]*n,t}},{}],217:[function(t,e,r){e.exports=function(t,e,r,n,i){return t[0]=e,t[1]=r,t[2]=n,t[3]=i,t}},{}],218:[function(t,e,r){e.exports=function(t,e){var r=e[0]-t[0],n=e[1]-t[1],i=e[2]-t[2],a=e[3]-t[3];return r*r+n*n+i*i+a*a}},{}],219:[function(t,e,r){e.exports=function(t){var e=t[0],r=t[1],n=t[2],i=t[3];return e*e+r*r+n*n+i*i}},{}],220:[function(t,e,r){e.exports=function(t,e,r){return t[0]=e[0]-r[0],t[1]=e[1]-r[1],t[2]=e[2]-r[2],t[3]=e[3]-r[3],t}},{}],221:[function(t,e,r){e.exports=function(t,e,r){var n=e[0],i=e[1],a=e[2],o=e[3];return t[0]=r[0]*n+r[4]*i+r[8]*a+r[12]*o,t[1]=r[1]*n+r[5]*i+r[9]*a+r[13]*o,t[2]=r[2]*n+r[6]*i+r[10]*a+r[14]*o,t[3]=r[3]*n+r[7]*i+r[11]*a+r[15]*o,t}},{}],222:[function(t,e,r){e.exports=function(t,e,r){var n=e[0],i=e[1],a=e[2],o=r[0],s=r[1],l=r[2],c=r[3],u=c*n+s*a-l*i,f=c*i+l*n-o*a,h=c*a+o*i-s*n,p=-o*n-s*i-l*a;return t[0]=u*c+p*-o+f*-l-h*-s,t[1]=f*c+p*-s+h*-o-u*-l,t[2]=h*c+p*-l+u*-s-f*-o,t[3]=e[3],t}},{}],223:[function(t,e,r){var n=t("glsl-tokenizer"),i=t("atob-lite");e.exports=function(t){for(var e=Array.isArray(t)?t:n(t),r=0;r0)continue;r=t.slice(0,1).join("")}return A(r),v+=r.length,(p=p.slice(r.length)).length}}function I(){return/[^a-fA-F0-9]/.test(e)?(A(p.join("")),h=999,u):(p.push(e),r=e,u+1)}function O(){return"."===e||/[eE]/.test(e)?(p.push(e),h=5,r=e,u+1):"x"===e&&1===p.length&&"0"===p[0]?(h=11,p.push(e),r=e,u+1):/[^\d]/.test(e)?(A(p.join("")),h=999,u):(p.push(e),r=e,u+1)}function z(){return"f"===e&&(p.push(e),r=e,u+=1),/[eE]/.test(e)?(p.push(e),r=e,u+1):("-"!==e&&"+"!==e||!/[eE]/.test(r))&&/[^\d]/.test(e)?(A(p.join("")),h=999,u):(p.push(e),r=e,u+1)}function D(){if(/[^\d\w_]/.test(e)){var t=p.join("");return h=k[t]?8:T[t]?7:6,A(p.join("")),h=999,u}return p.push(e),r=e,u+1}};var n=t("./lib/literals"),i=t("./lib/operators"),a=t("./lib/builtins"),o=t("./lib/literals-300es"),s=t("./lib/builtins-300es"),l=["block-comment","line-comment","preprocessor","operator","integer","float","ident","builtin","keyword","whitespace","eof","integer"]},{"./lib/builtins":226,"./lib/builtins-300es":225,"./lib/literals":228,"./lib/literals-300es":227,"./lib/operators":229}],225:[function(t,e,r){var n=t("./builtins");n=n.slice().filter((function(t){return!/^(gl\_|texture)/.test(t)})),e.exports=n.concat(["gl_VertexID","gl_InstanceID","gl_Position","gl_PointSize","gl_FragCoord","gl_FrontFacing","gl_FragDepth","gl_PointCoord","gl_MaxVertexAttribs","gl_MaxVertexUniformVectors","gl_MaxVertexOutputVectors","gl_MaxFragmentInputVectors","gl_MaxVertexTextureImageUnits","gl_MaxCombinedTextureImageUnits","gl_MaxTextureImageUnits","gl_MaxFragmentUniformVectors","gl_MaxDrawBuffers","gl_MinProgramTexelOffset","gl_MaxProgramTexelOffset","gl_DepthRangeParameters","gl_DepthRange","trunc","round","roundEven","isnan","isinf","floatBitsToInt","floatBitsToUint","intBitsToFloat","uintBitsToFloat","packSnorm2x16","unpackSnorm2x16","packUnorm2x16","unpackUnorm2x16","packHalf2x16","unpackHalf2x16","outerProduct","transpose","determinant","inverse","texture","textureSize","textureProj","textureLod","textureOffset","texelFetch","texelFetchOffset","textureProjOffset","textureLodOffset","textureProjLod","textureProjLodOffset","textureGrad","textureGradOffset","textureProjGrad","textureProjGradOffset"])},{"./builtins":226}],226:[function(t,e,r){e.exports=["abs","acos","all","any","asin","atan","ceil","clamp","cos","cross","dFdx","dFdy","degrees","distance","dot","equal","exp","exp2","faceforward","floor","fract","gl_BackColor","gl_BackLightModelProduct","gl_BackLightProduct","gl_BackMaterial","gl_BackSecondaryColor","gl_ClipPlane","gl_ClipVertex","gl_Color","gl_DepthRange","gl_DepthRangeParameters","gl_EyePlaneQ","gl_EyePlaneR","gl_EyePlaneS","gl_EyePlaneT","gl_Fog","gl_FogCoord","gl_FogFragCoord","gl_FogParameters","gl_FragColor","gl_FragCoord","gl_FragData","gl_FragDepth","gl_FragDepthEXT","gl_FrontColor","gl_FrontFacing","gl_FrontLightModelProduct","gl_FrontLightProduct","gl_FrontMaterial","gl_FrontSecondaryColor","gl_LightModel","gl_LightModelParameters","gl_LightModelProducts","gl_LightProducts","gl_LightSource","gl_LightSourceParameters","gl_MaterialParameters","gl_MaxClipPlanes","gl_MaxCombinedTextureImageUnits","gl_MaxDrawBuffers","gl_MaxFragmentUniformComponents","gl_MaxLights","gl_MaxTextureCoords","gl_MaxTextureImageUnits","gl_MaxTextureUnits","gl_MaxVaryingFloats","gl_MaxVertexAttribs","gl_MaxVertexTextureImageUnits","gl_MaxVertexUniformComponents","gl_ModelViewMatrix","gl_ModelViewMatrixInverse","gl_ModelViewMatrixInverseTranspose","gl_ModelViewMatrixTranspose","gl_ModelViewProjectionMatrix","gl_ModelViewProjectionMatrixInverse","gl_ModelViewProjectionMatrixInverseTranspose","gl_ModelViewProjectionMatrixTranspose","gl_MultiTexCoord0","gl_MultiTexCoord1","gl_MultiTexCoord2","gl_MultiTexCoord3","gl_MultiTexCoord4","gl_MultiTexCoord5","gl_MultiTexCoord6","gl_MultiTexCoord7","gl_Normal","gl_NormalMatrix","gl_NormalScale","gl_ObjectPlaneQ","gl_ObjectPlaneR","gl_ObjectPlaneS","gl_ObjectPlaneT","gl_Point","gl_PointCoord","gl_PointParameters","gl_PointSize","gl_Position","gl_ProjectionMatrix","gl_ProjectionMatrixInverse","gl_ProjectionMatrixInverseTranspose","gl_ProjectionMatrixTranspose","gl_SecondaryColor","gl_TexCoord","gl_TextureEnvColor","gl_TextureMatrix","gl_TextureMatrixInverse","gl_TextureMatrixInverseTranspose","gl_TextureMatrixTranspose","gl_Vertex","greaterThan","greaterThanEqual","inversesqrt","length","lessThan","lessThanEqual","log","log2","matrixCompMult","max","min","mix","mod","normalize","not","notEqual","pow","radians","reflect","refract","sign","sin","smoothstep","sqrt","step","tan","texture2D","texture2DLod","texture2DProj","texture2DProjLod","textureCube","textureCubeLod","texture2DLodEXT","texture2DProjLodEXT","textureCubeLodEXT","texture2DGradEXT","texture2DProjGradEXT","textureCubeGradEXT"]},{}],227:[function(t,e,r){var n=t("./literals");e.exports=n.slice().concat(["layout","centroid","smooth","case","mat2x2","mat2x3","mat2x4","mat3x2","mat3x3","mat3x4","mat4x2","mat4x3","mat4x4","uvec2","uvec3","uvec4","samplerCubeShadow","sampler2DArray","sampler2DArrayShadow","isampler2D","isampler3D","isamplerCube","isampler2DArray","usampler2D","usampler3D","usamplerCube","usampler2DArray","coherent","restrict","readonly","writeonly","resource","atomic_uint","noperspective","patch","sample","subroutine","common","partition","active","filter","image1D","image2D","image3D","imageCube","iimage1D","iimage2D","iimage3D","iimageCube","uimage1D","uimage2D","uimage3D","uimageCube","image1DArray","image2DArray","iimage1DArray","iimage2DArray","uimage1DArray","uimage2DArray","image1DShadow","image2DShadow","image1DArrayShadow","image2DArrayShadow","imageBuffer","iimageBuffer","uimageBuffer","sampler1DArray","sampler1DArrayShadow","isampler1D","isampler1DArray","usampler1D","usampler1DArray","isampler2DRect","usampler2DRect","samplerBuffer","isamplerBuffer","usamplerBuffer","sampler2DMS","isampler2DMS","usampler2DMS","sampler2DMSArray","isampler2DMSArray","usampler2DMSArray"])},{"./literals":228}],228:[function(t,e,r){e.exports=["precision","highp","mediump","lowp","attribute","const","uniform","varying","break","continue","do","for","while","if","else","in","out","inout","float","int","uint","void","bool","true","false","discard","return","mat2","mat3","mat4","vec2","vec3","vec4","ivec2","ivec3","ivec4","bvec2","bvec3","bvec4","sampler1D","sampler2D","sampler3D","samplerCube","sampler1DShadow","sampler2DShadow","struct","asm","class","union","enum","typedef","template","this","packed","goto","switch","default","inline","noinline","volatile","public","static","extern","external","interface","long","short","double","half","fixed","unsigned","input","output","hvec2","hvec3","hvec4","dvec2","dvec3","dvec4","fvec2","fvec3","fvec4","sampler2DRect","sampler3DRect","sampler2DRectShadow","sizeof","cast","namespace","using"]},{}],229:[function(t,e,r){e.exports=["<<=",">>=","++","--","<<",">>","<=",">=","==","!=","&&","||","+=","-=","*=","/=","%=","&=","^^","^=","|=","(",")","[","]",".","!","~","*","/","%","+","-","<",">","&","^","|","?",":","=",",",";","{","}"]},{}],230:[function(t,e,r){var n=t("./index");e.exports=function(t,e){var r=n(e),i=[];return i=(i=i.concat(r(t))).concat(r(null))}},{"./index":224}],231:[function(t,e,r){e.exports=function(t){"string"==typeof t&&(t=[t]);for(var e=[].slice.call(arguments,1),r=[],n=0;n0;)for(var s=(t=o.pop()).adjacent,l=0;l<=r;++l){var c=s[l];if(c.boundary&&!(c.lastVisited<=-n)){for(var u=c.vertices,f=0;f<=r;++f){var h=u[f];i[f]=h<0?e:a[h]}var p=this.orient();if(p>0)return c;c.lastVisited=-n,0===p&&o.push(c)}}return null},u.walk=function(t,e){var r=this.vertices.length-1,n=this.dimension,i=this.vertices,a=this.tuple,o=e?this.interior.length*Math.random()|0:this.interior.length-1,s=this.interior[o];t:for(;!s.boundary;){for(var l=s.vertices,c=s.adjacent,u=0;u<=n;++u)a[u]=i[l[u]];s.lastVisited=r;for(u=0;u<=n;++u){var f=c[u];if(!(f.lastVisited>=r)){var h=a[u];a[u]=t;var p=this.orient();if(a[u]=h,p<0){s=f;continue t}f.boundary?f.lastVisited=-r:f.lastVisited=r}}return}return s},u.addPeaks=function(t,e){var r=this.vertices.length-1,n=this.dimension,i=this.vertices,l=this.tuple,c=this.interior,u=this.simplices,f=[e];e.lastVisited=r,e.vertices[e.vertices.indexOf(-1)]=r,e.boundary=!1,c.push(e);for(var h=[];f.length>0;){var p=(e=f.pop()).vertices,d=e.adjacent,m=p.indexOf(r);if(!(m<0))for(var g=0;g<=n;++g)if(g!==m){var v=d[g];if(v.boundary&&!(v.lastVisited>=r)){var y=v.vertices;if(v.lastVisited!==-r){for(var x=0,b=0;b<=n;++b)y[b]<0?(x=b,l[b]=t):l[b]=i[y[b]];if(this.orient()>0){y[x]=r,v.boundary=!1,c.push(v),f.push(v),v.lastVisited=r;continue}v.lastVisited=-r}var _=v.adjacent,w=p.slice(),T=d.slice(),k=new a(w,T,!0);u.push(k);var A=_.indexOf(e);if(!(A<0)){_[A]=k,T[m]=v,w[g]=-1,T[g]=e,d[g]=k,k.flip();for(b=0;b<=n;++b){var M=w[b];if(!(M<0||M===r)){for(var S=new Array(n-1),E=0,L=0;L<=n;++L){var C=w[L];C<0||L===b||(S[E++]=C)}h.push(new o(S,k,b))}}}}}}h.sort(s);for(g=0;g+1=0?o[l++]=s[u]:c=1&u;if(c===(1&t)){var f=o[0];o[0]=o[1],o[1]=f}e.push(o)}}return e}},{"robust-orientation":284,"simplicial-complex":293}],234:[function(t,e,r){"use strict";var n=t("binary-search-bounds");function i(t,e,r,n,i){this.mid=t,this.left=e,this.right=r,this.leftPoints=n,this.rightPoints=i,this.count=(e?e.count:0)+(r?r.count:0)+n.length}e.exports=function(t){if(!t||0===t.length)return new v(null);return new v(g(t))};var a=i.prototype;function o(t,e){t.mid=e.mid,t.left=e.left,t.right=e.right,t.leftPoints=e.leftPoints,t.rightPoints=e.rightPoints,t.count=e.count}function s(t,e){var r=g(e);t.mid=r.mid,t.left=r.left,t.right=r.right,t.leftPoints=r.leftPoints,t.rightPoints=r.rightPoints,t.count=r.count}function l(t,e){var r=t.intervals([]);r.push(e),s(t,r)}function c(t,e){var r=t.intervals([]),n=r.indexOf(e);return n<0?0:(r.splice(n,1),s(t,r),1)}function u(t,e,r){for(var n=0;n=0&&t[n][1]>=e;--n){var i=r(t[n]);if(i)return i}}function h(t,e){for(var r=0;r>1],a=[],o=[],s=[];for(r=0;r3*(e+1)?l(this,t):this.left.insert(t):this.left=g([t]);else if(t[0]>this.mid)this.right?4*(this.right.count+1)>3*(e+1)?l(this,t):this.right.insert(t):this.right=g([t]);else{var r=n.ge(this.leftPoints,t,d),i=n.ge(this.rightPoints,t,m);this.leftPoints.splice(r,0,t),this.rightPoints.splice(i,0,t)}},a.remove=function(t){var e=this.count-this.leftPoints;if(t[1]3*(e-1)?c(this,t):2===(s=this.left.remove(t))?(this.left=null,this.count-=1,1):(1===s&&(this.count-=1),s):0;if(t[0]>this.mid)return this.right?4*(this.left?this.left.count:0)>3*(e-1)?c(this,t):2===(s=this.right.remove(t))?(this.right=null,this.count-=1,1):(1===s&&(this.count-=1),s):0;if(1===this.count)return this.leftPoints[0]===t?2:0;if(1===this.leftPoints.length&&this.leftPoints[0]===t){if(this.left&&this.right){for(var r=this,i=this.left;i.right;)r=i,i=i.right;if(r===this)i.right=this.right;else{var a=this.left,s=this.right;r.count-=i.count,r.right=i.left,i.left=a,i.right=s}o(this,i),this.count=(this.left?this.left.count:0)+(this.right?this.right.count:0)+this.leftPoints.length}else this.left?o(this,this.left):o(this,this.right);return 1}for(a=n.ge(this.leftPoints,t,d);athis.mid){var r;if(this.right)if(r=this.right.queryPoint(t,e))return r;return f(this.rightPoints,t,e)}return h(this.leftPoints,e)},a.queryInterval=function(t,e,r){var n;if(tthis.mid&&this.right&&(n=this.right.queryInterval(t,e,r)))return n;return ethis.mid?f(this.rightPoints,t,r):h(this.leftPoints,r)};var y=v.prototype;y.insert=function(t){this.root?this.root.insert(t):this.root=new i(t[0],null,null,[t],[t])},y.remove=function(t){if(this.root){var e=this.root.remove(t);return 2===e&&(this.root=null),0!==e}return!1},y.queryPoint=function(t,e){if(this.root)return this.root.queryPoint(t,e)},y.queryInterval=function(t,e,r){if(t<=e&&this.root)return this.root.queryInterval(t,e,r)},Object.defineProperty(y,"count",{get:function(){return this.root?this.root.count:0}}),Object.defineProperty(y,"intervals",{get:function(){return this.root?this.root.intervals([]):[]}})},{"binary-search-bounds":31}],235:[function(t,e,r){"use strict";e.exports=function(t){for(var e=new Array(t),r=0;r + * @license MIT + */ +e.exports=function(t){return null!=t&&(n(t)||function(t){return"function"==typeof t.readFloatLE&&"function"==typeof t.slice&&n(t.slice(0,0))}(t)||!!t._isBuffer)}},{}],238:[function(t,e,r){"use strict";e.exports=a,e.exports.isMobile=a,e.exports.default=a;var n=/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series[46]0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i,i=/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series[46]0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino|android|ipad|playbook|silk/i;function a(t){t||(t={});var e=t.ua;if(e||"undefined"==typeof navigator||(e=navigator.userAgent),e&&e.headers&&"string"==typeof e.headers["user-agent"]&&(e=e.headers["user-agent"]),"string"!=typeof e)return!1;var r=t.tablet?i.test(e):n.test(e);return!r&&t.tablet&&t.featureDetect&&navigator&&navigator.maxTouchPoints>1&&-1!==e.indexOf("Macintosh")&&-1!==e.indexOf("Safari")&&(r=!0),r}},{}],239:[function(t,e,r){"use strict";e.exports=function(t){for(var e,r=t.length,n=0;n13)&&32!==e&&133!==e&&160!==e&&5760!==e&&6158!==e&&(e<8192||e>8205)&&8232!==e&&8233!==e&&8239!==e&&8287!==e&&8288!==e&&12288!==e&&65279!==e)return!1;return!0}},{}],240:[function(t,e,r){e.exports=function(t,e,r){return t*(1-r)+e*r}},{}],241:[function(t,e,r){var n=t("./normalize"),i=t("gl-mat4/create"),a=t("gl-mat4/clone"),o=t("gl-mat4/determinant"),s=t("gl-mat4/invert"),l=t("gl-mat4/transpose"),c={length:t("gl-vec3/length"),normalize:t("gl-vec3/normalize"),dot:t("gl-vec3/dot"),cross:t("gl-vec3/cross")},u=i(),f=i(),h=[0,0,0,0],p=[[0,0,0],[0,0,0],[0,0,0]],d=[0,0,0];function m(t,e,r,n,i){t[0]=e[0]*n+r[0]*i,t[1]=e[1]*n+r[1]*i,t[2]=e[2]*n+r[2]*i}e.exports=function(t,e,r,i,g,v){if(e||(e=[0,0,0]),r||(r=[0,0,0]),i||(i=[0,0,0]),g||(g=[0,0,0,1]),v||(v=[0,0,0,1]),!n(u,t))return!1;if(a(f,u),f[3]=0,f[7]=0,f[11]=0,f[15]=1,Math.abs(o(f)<1e-8))return!1;var y,x,b,_,w,T,k,A=u[3],M=u[7],S=u[11],E=u[12],L=u[13],C=u[14],P=u[15];if(0!==A||0!==M||0!==S){if(h[0]=A,h[1]=M,h[2]=S,h[3]=P,!s(f,f))return!1;l(f,f),y=g,b=f,_=(x=h)[0],w=x[1],T=x[2],k=x[3],y[0]=b[0]*_+b[4]*w+b[8]*T+b[12]*k,y[1]=b[1]*_+b[5]*w+b[9]*T+b[13]*k,y[2]=b[2]*_+b[6]*w+b[10]*T+b[14]*k,y[3]=b[3]*_+b[7]*w+b[11]*T+b[15]*k}else g[0]=g[1]=g[2]=0,g[3]=1;if(e[0]=E,e[1]=L,e[2]=C,function(t,e){t[0][0]=e[0],t[0][1]=e[1],t[0][2]=e[2],t[1][0]=e[4],t[1][1]=e[5],t[1][2]=e[6],t[2][0]=e[8],t[2][1]=e[9],t[2][2]=e[10]}(p,u),r[0]=c.length(p[0]),c.normalize(p[0],p[0]),i[0]=c.dot(p[0],p[1]),m(p[1],p[1],p[0],1,-i[0]),r[1]=c.length(p[1]),c.normalize(p[1],p[1]),i[0]/=r[1],i[1]=c.dot(p[0],p[2]),m(p[2],p[2],p[0],1,-i[1]),i[2]=c.dot(p[1],p[2]),m(p[2],p[2],p[1],1,-i[2]),r[2]=c.length(p[2]),c.normalize(p[2],p[2]),i[1]/=r[2],i[2]/=r[2],c.cross(d,p[1],p[2]),c.dot(p[0],d)<0)for(var I=0;I<3;I++)r[I]*=-1,p[I][0]*=-1,p[I][1]*=-1,p[I][2]*=-1;return v[0]=.5*Math.sqrt(Math.max(1+p[0][0]-p[1][1]-p[2][2],0)),v[1]=.5*Math.sqrt(Math.max(1-p[0][0]+p[1][1]-p[2][2],0)),v[2]=.5*Math.sqrt(Math.max(1-p[0][0]-p[1][1]+p[2][2],0)),v[3]=.5*Math.sqrt(Math.max(1+p[0][0]+p[1][1]+p[2][2],0)),p[2][1]>p[1][2]&&(v[0]=-v[0]),p[0][2]>p[2][0]&&(v[1]=-v[1]),p[1][0]>p[0][1]&&(v[2]=-v[2]),!0}},{"./normalize":242,"gl-mat4/clone":92,"gl-mat4/create":93,"gl-mat4/determinant":94,"gl-mat4/invert":98,"gl-mat4/transpose":109,"gl-vec3/cross":157,"gl-vec3/dot":162,"gl-vec3/length":172,"gl-vec3/normalize":179}],242:[function(t,e,r){e.exports=function(t,e){var r=e[15];if(0===r)return!1;for(var n=1/r,i=0;i<16;i++)t[i]=e[i]*n;return!0}},{}],243:[function(t,e,r){var n=t("gl-vec3/lerp"),i=t("mat4-recompose"),a=t("mat4-decompose"),o=t("gl-mat4/determinant"),s=t("quat-slerp"),l=f(),c=f(),u=f();function f(){return{translate:h(),scale:h(1),skew:h(),perspective:[0,0,0,1],quaternion:[0,0,0,1]}}function h(t){return[t||0,t||0,t||0]}e.exports=function(t,e,r,f){if(0===o(e)||0===o(r))return!1;var h=a(e,l.translate,l.scale,l.skew,l.perspective,l.quaternion),p=a(r,c.translate,c.scale,c.skew,c.perspective,c.quaternion);return!(!h||!p)&&(n(u.translate,l.translate,c.translate,f),n(u.skew,l.skew,c.skew,f),n(u.scale,l.scale,c.scale,f),n(u.perspective,l.perspective,c.perspective,f),s(u.quaternion,l.quaternion,c.quaternion,f),i(t,u.translate,u.scale,u.skew,u.perspective,u.quaternion),!0)}},{"gl-mat4/determinant":94,"gl-vec3/lerp":173,"mat4-decompose":241,"mat4-recompose":244,"quat-slerp":271}],244:[function(t,e,r){var n={identity:t("gl-mat4/identity"),translate:t("gl-mat4/translate"),multiply:t("gl-mat4/multiply"),create:t("gl-mat4/create"),scale:t("gl-mat4/scale"),fromRotationTranslation:t("gl-mat4/fromRotationTranslation")},i=(n.create(),n.create());e.exports=function(t,e,r,a,o,s){return n.identity(t),n.fromRotationTranslation(t,s,e),t[3]=o[0],t[7]=o[1],t[11]=o[2],t[15]=o[3],n.identity(i),0!==a[2]&&(i[9]=a[2],n.multiply(t,t,i)),0!==a[1]&&(i[9]=0,i[8]=a[1],n.multiply(t,t,i)),0!==a[0]&&(i[8]=0,i[4]=a[0],n.multiply(t,t,i)),n.scale(t,t,r),t}},{"gl-mat4/create":93,"gl-mat4/fromRotationTranslation":96,"gl-mat4/identity":97,"gl-mat4/multiply":100,"gl-mat4/scale":107,"gl-mat4/translate":108}],245:[function(t,e,r){"use strict";var n=t("binary-search-bounds"),i=t("mat4-interpolate"),a=t("gl-mat4/invert"),o=t("gl-mat4/rotateX"),s=t("gl-mat4/rotateY"),l=t("gl-mat4/rotateZ"),c=t("gl-mat4/lookAt"),u=t("gl-mat4/translate"),f=(t("gl-mat4/scale"),t("gl-vec3/normalize")),h=[0,0,0];function p(t){this._components=t.slice(),this._time=[0],this.prevMatrix=t.slice(),this.nextMatrix=t.slice(),this.computedMatrix=t.slice(),this.computedInverse=t.slice(),this.computedEye=[0,0,0],this.computedUp=[0,0,0],this.computedCenter=[0,0,0],this.computedRadius=[0],this._limits=[-1/0,1/0]}e.exports=function(t){return new p((t=t||{}).matrix||[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1])};var d=p.prototype;d.recalcMatrix=function(t){var e=this._time,r=n.le(e,t),o=this.computedMatrix;if(!(r<0)){var s=this._components;if(r===e.length-1)for(var l=16*r,c=0;c<16;++c)o[c]=s[l++];else{var u=e[r+1]-e[r],h=(l=16*r,this.prevMatrix),p=!0;for(c=0;c<16;++c)h[c]=s[l++];var d=this.nextMatrix;for(c=0;c<16;++c)d[c]=s[l++],p=p&&h[c]===d[c];if(u<1e-6||p)for(c=0;c<16;++c)o[c]=h[c];else i(o,h,d,(t-e[r])/u)}var m=this.computedUp;m[0]=o[1],m[1]=o[5],m[2]=o[9],f(m,m);var g=this.computedInverse;a(g,o);var v=this.computedEye,y=g[15];v[0]=g[12]/y,v[1]=g[13]/y,v[2]=g[14]/y;var x=this.computedCenter,b=Math.exp(this.computedRadius[0]);for(c=0;c<3;++c)x[c]=v[c]-o[2+4*c]*b}},d.idle=function(t){if(!(t1&&n(t[o[u-2]],t[o[u-1]],c)<=0;)u-=1,o.pop();for(o.push(l),u=s.length;u>1&&n(t[s[u-2]],t[s[u-1]],c)>=0;)u-=1,s.pop();s.push(l)}r=new Array(s.length+o.length-2);for(var f=0,h=(i=0,o.length);i0;--p)r[f++]=s[p];return r};var n=t("robust-orientation")[3]},{"robust-orientation":284}],247:[function(t,e,r){"use strict";e.exports=function(t,e){e||(e=t,t=window);var r=0,i=0,a=0,o={shift:!1,alt:!1,control:!1,meta:!1},s=!1;function l(t){var e=!1;return"altKey"in t&&(e=e||t.altKey!==o.alt,o.alt=!!t.altKey),"shiftKey"in t&&(e=e||t.shiftKey!==o.shift,o.shift=!!t.shiftKey),"ctrlKey"in t&&(e=e||t.ctrlKey!==o.control,o.control=!!t.ctrlKey),"metaKey"in t&&(e=e||t.metaKey!==o.meta,o.meta=!!t.metaKey),e}function c(t,s){var c=n.x(s),u=n.y(s);"buttons"in s&&(t=0|s.buttons),(t!==r||c!==i||u!==a||l(s))&&(r=0|t,i=c||0,a=u||0,e&&e(r,i,a,o))}function u(t){c(0,t)}function f(){(r||i||a||o.shift||o.alt||o.meta||o.control)&&(i=a=0,r=0,o.shift=o.alt=o.control=o.meta=!1,e&&e(0,0,0,o))}function h(t){l(t)&&e&&e(r,i,a,o)}function p(t){0===n.buttons(t)?c(0,t):c(r,t)}function d(t){c(r|n.buttons(t),t)}function m(t){c(r&~n.buttons(t),t)}function g(){s||(s=!0,t.addEventListener("mousemove",p),t.addEventListener("mousedown",d),t.addEventListener("mouseup",m),t.addEventListener("mouseleave",u),t.addEventListener("mouseenter",u),t.addEventListener("mouseout",u),t.addEventListener("mouseover",u),t.addEventListener("blur",f),t.addEventListener("keyup",h),t.addEventListener("keydown",h),t.addEventListener("keypress",h),t!==window&&(window.addEventListener("blur",f),window.addEventListener("keyup",h),window.addEventListener("keydown",h),window.addEventListener("keypress",h)))}g();var v={element:t};return Object.defineProperties(v,{enabled:{get:function(){return s},set:function(e){e?g():function(){if(!s)return;s=!1,t.removeEventListener("mousemove",p),t.removeEventListener("mousedown",d),t.removeEventListener("mouseup",m),t.removeEventListener("mouseleave",u),t.removeEventListener("mouseenter",u),t.removeEventListener("mouseout",u),t.removeEventListener("mouseover",u),t.removeEventListener("blur",f),t.removeEventListener("keyup",h),t.removeEventListener("keydown",h),t.removeEventListener("keypress",h),t!==window&&(window.removeEventListener("blur",f),window.removeEventListener("keyup",h),window.removeEventListener("keydown",h),window.removeEventListener("keypress",h))}()},enumerable:!0},buttons:{get:function(){return r},enumerable:!0},x:{get:function(){return i},enumerable:!0},y:{get:function(){return a},enumerable:!0},mods:{get:function(){return o},enumerable:!0}}),v};var n=t("mouse-event")},{"mouse-event":249}],248:[function(t,e,r){var n={left:0,top:0};e.exports=function(t,e,r){e=e||t.currentTarget||t.srcElement,Array.isArray(r)||(r=[0,0]);var i=t.clientX||0,a=t.clientY||0,o=(s=e,s===window||s===document||s===document.body?n:s.getBoundingClientRect());var s;return r[0]=i-o.left,r[1]=a-o.top,r}},{}],249:[function(t,e,r){"use strict";function n(t){return t.target||t.srcElement||window}r.buttons=function(t){if("object"==typeof t){if("buttons"in t)return t.buttons;if("which"in t){if(2===(e=t.which))return 4;if(3===e)return 2;if(e>0)return 1<=0)return 1< 0");"function"!=typeof t.vertex&&e("Must specify vertex creation function");"function"!=typeof t.cell&&e("Must specify cell creation function");"function"!=typeof t.phase&&e("Must specify phase function");for(var s=t.getters||[],l=new Array(a),c=0;c=0?l[c]=!0:l[c]=!1;return function(t,e,r,a,o,s){var l=[s,o].join(",");return(0,i[l])(t,e,r,n.mallocUint32,n.freeUint32)}(t.vertex,t.cell,t.phase,0,r,l)};var i={"false,0,1":function(t,e,r,n,i){return function(a,o,s,l){var c,u=0|a.shape[0],f=0|a.shape[1],h=a.data,p=0|a.offset,d=0|a.stride[0],m=0|a.stride[1],g=p,v=0|-d,y=0,x=0|-m,b=0,_=-d-m|0,w=0,T=0|d,k=m-d*u|0,A=0,M=0,S=0,E=2*u|0,L=n(E),C=n(E),P=0,I=0,O=-1,z=-1,D=0,R=0|-u,F=0|u,B=0,N=-u-1|0,j=u-1|0,U=0,V=0,H=0;for(A=0;A0){if(M=1,L[P++]=r(h[g],o,s,l),g+=T,u>0)for(A=1,c=h[g],I=L[P]=r(c,o,s,l),D=L[P+O],B=L[P+R],U=L[P+N],I===D&&I===B&&I===U||(y=h[g+v],b=h[g+x],w=h[g+_],t(A,M,c,y,b,w,I,D,B,U,o,s,l),V=C[P]=S++),P+=1,g+=T,A=2;A0)for(A=1,c=h[g],I=L[P]=r(c,o,s,l),D=L[P+O],B=L[P+R],U=L[P+N],I===D&&I===B&&I===U||(y=h[g+v],b=h[g+x],w=h[g+_],t(A,M,c,y,b,w,I,D,B,U,o,s,l),V=C[P]=S++,U!==B&&e(C[P+R],V,b,w,B,U,o,s,l)),P+=1,g+=T,A=2;A0){if(A=1,L[P++]=r(h[g],o,s,l),g+=T,f>0)for(M=1,c=h[g],I=L[P]=r(c,o,s,l),B=L[P+R],D=L[P+O],U=L[P+N],I===B&&I===D&&I===U||(y=h[g+v],b=h[g+x],w=h[g+_],t(A,M,c,y,b,w,I,B,D,U,o,s,l),V=C[P]=S++),P+=1,g+=T,M=2;M0)for(M=1,c=h[g],I=L[P]=r(c,o,s,l),B=L[P+R],D=L[P+O],U=L[P+N],I===B&&I===D&&I===U||(y=h[g+v],b=h[g+x],w=h[g+_],t(A,M,c,y,b,w,I,B,D,U,o,s,l),V=C[P]=S++,U!==B&&e(C[P+R],V,w,y,U,B,o,s,l)),P+=1,g+=T,M=2;M2&&a[1]>2&&n(i.pick(-1,-1).lo(1,1).hi(a[0]-2,a[1]-2),t.pick(-1,-1,0).lo(1,1).hi(a[0]-2,a[1]-2),t.pick(-1,-1,1).lo(1,1).hi(a[0]-2,a[1]-2)),a[1]>2&&(r(i.pick(0,-1).lo(1).hi(a[1]-2),t.pick(0,-1,1).lo(1).hi(a[1]-2)),e(t.pick(0,-1,0).lo(1).hi(a[1]-2))),a[1]>2&&(r(i.pick(a[0]-1,-1).lo(1).hi(a[1]-2),t.pick(a[0]-1,-1,1).lo(1).hi(a[1]-2)),e(t.pick(a[0]-1,-1,0).lo(1).hi(a[1]-2))),a[0]>2&&(r(i.pick(-1,0).lo(1).hi(a[0]-2),t.pick(-1,0,0).lo(1).hi(a[0]-2)),e(t.pick(-1,0,1).lo(1).hi(a[0]-2))),a[0]>2&&(r(i.pick(-1,a[1]-1).lo(1).hi(a[0]-2),t.pick(-1,a[1]-1,0).lo(1).hi(a[0]-2)),e(t.pick(-1,a[1]-1,1).lo(1).hi(a[0]-2))),t.set(0,0,0,0),t.set(0,0,1,0),t.set(a[0]-1,0,0,0),t.set(a[0]-1,0,1,0),t.set(0,a[1]-1,0,0),t.set(0,a[1]-1,1,0),t.set(a[0]-1,a[1]-1,0,0),t.set(a[0]-1,a[1]-1,1,0),t}}e.exports=function(t,e,r){return Array.isArray(r)||(r=n(e.dimension,"string"==typeof r?r:"clamp")),0===e.size?t:0===e.dimension?(t.set(0),t):function(t){var e=t.join();if(a=u[e])return a;for(var r=t.length,n=[f,h],i=1;i<=r;++i)n.push(p(i));var a=d.apply(void 0,n);return u[e]=a,a}(r)(t,e)}},{dup:65}],253:[function(t,e,r){"use strict";function n(t,e){var r=Math.floor(e),n=e-r,i=0<=r&&r0;){x<64?(l=x,x=0):(l=64,x-=64);for(var b=0|t[1];b>0;){b<64?(c=b,b=0):(c=64,b-=64),n=v+x*f+b*h,o=y+x*d+b*m;var _=0,w=0,T=0,k=p,A=f-u*p,M=h-l*f,S=g,E=d-u*g,L=m-l*d;for(T=0;T0;){m<64?(l=m,m=0):(l=64,m-=64);for(var g=0|t[0];g>0;){g<64?(s=g,g=0):(s=64,g-=64),n=p+m*u+g*c,o=d+m*h+g*f;var v=0,y=0,x=u,b=c-l*u,_=h,w=f-l*h;for(y=0;y0;){y<64?(c=y,y=0):(c=64,y-=64);for(var x=0|t[0];x>0;){x<64?(s=x,x=0):(s=64,x-=64);for(var b=0|t[1];b>0;){b<64?(l=b,b=0):(l=64,b-=64),n=g+y*h+x*u+b*f,o=v+y*m+x*p+b*d;var _=0,w=0,T=0,k=h,A=u-c*h,M=f-s*u,S=m,E=p-c*m,L=d-s*p;for(T=0;Tr;){v=0,y=m-o;e:for(g=0;gb)break e;y+=f,v+=h}for(v=m,y=m-o,g=0;g>1,q=H-j,G=H+j,Y=U,W=q,X=H,Z=G,J=V,K=i+1,Q=a-1,$=!0,tt=0,et=0,rt=0,nt=f,it=e(nt),at=e(nt);A=l*Y,M=l*W,N=s;t:for(k=0;k0){g=Y,Y=W,W=g;break t}if(rt<0)break t;N+=p}A=l*Z,M=l*J,N=s;t:for(k=0;k0){g=Z,Z=J,J=g;break t}if(rt<0)break t;N+=p}A=l*Y,M=l*X,N=s;t:for(k=0;k0){g=Y,Y=X,X=g;break t}if(rt<0)break t;N+=p}A=l*W,M=l*X,N=s;t:for(k=0;k0){g=W,W=X,X=g;break t}if(rt<0)break t;N+=p}A=l*Y,M=l*Z,N=s;t:for(k=0;k0){g=Y,Y=Z,Z=g;break t}if(rt<0)break t;N+=p}A=l*X,M=l*Z,N=s;t:for(k=0;k0){g=X,X=Z,Z=g;break t}if(rt<0)break t;N+=p}A=l*W,M=l*J,N=s;t:for(k=0;k0){g=W,W=J,J=g;break t}if(rt<0)break t;N+=p}A=l*W,M=l*X,N=s;t:for(k=0;k0){g=W,W=X,X=g;break t}if(rt<0)break t;N+=p}A=l*Z,M=l*J,N=s;t:for(k=0;k0){g=Z,Z=J,J=g;break t}if(rt<0)break t;N+=p}for(A=l*Y,M=l*W,S=l*X,E=l*Z,L=l*J,C=l*U,P=l*H,I=l*V,B=0,N=s,k=0;k0)){if(rt<0){for(A=l*b,M=l*K,S=l*Q,N=s,k=0;k0)for(;;){_=s+Q*l,B=0;t:for(k=0;k0)){_=s+Q*l,B=0;t:for(k=0;kV){t:for(;;){for(_=s+K*l,B=0,N=s,k=0;k1&&n?s(r,n[0],n[1]):s(r)}(t,e,l);return n(l,c)}},{"typedarray-pool":308}],258:[function(t,e,r){"use strict";var n=t("./lib/compile_sort.js"),i={};e.exports=function(t){var e=t.order,r=t.dtype,a=[e,r].join(":"),o=i[a];return o||(i[a]=o=n(e,r)),o(t),t}},{"./lib/compile_sort.js":257}],259:[function(t,e,r){var n=t("is-buffer"),i="undefined"!=typeof Float64Array;function a(t,e){return t[0]-e[0]}function o(){var t,e=this.stride,r=new Array(e.length);for(t=0;t=0&&(e+=a*(r=0|t),i-=r),new n(this.data,i,a,e)},i.step=function(t){var e=this.shape[0],r=this.stride[0],i=this.offset,a=0,o=Math.ceil;return"number"==typeof t&&((a=0|t)<0?(i+=r*(e-1),e=o(-e/a)):e=o(e/a),r*=a),new n(this.data,e,r,i)},i.transpose=function(t){t=void 0===t?0:0|t;var e=this.shape,r=this.stride;return new n(this.data,e[t],r[t],this.offset)},i.pick=function(t){var r=[],n=[],i=this.offset;return"number"==typeof t&&t>=0?i=i+this.stride[0]*t|0:(r.push(this.shape[0]),n.push(this.stride[0])),(0,e[r.length+1])(this.data,r,n,i)},function(t,e,r,i){return new n(t,e[0],r[0],i)}},2:function(t,e,r){function n(t,e,r,n,i,a){this.data=t,this.shape=[e,r],this.stride=[n,i],this.offset=0|a}var i=n.prototype;return i.dtype=t,i.dimension=2,Object.defineProperty(i,"size",{get:function(){return this.shape[0]*this.shape[1]}}),Object.defineProperty(i,"order",{get:function(){return Math.abs(this.stride[0])>Math.abs(this.stride[1])?[1,0]:[0,1]}}),i.set=function(e,r,n){return"generic"===t?this.data.set(this.offset+this.stride[0]*e+this.stride[1]*r,n):this.data[this.offset+this.stride[0]*e+this.stride[1]*r]=n},i.get=function(e,r){return"generic"===t?this.data.get(this.offset+this.stride[0]*e+this.stride[1]*r):this.data[this.offset+this.stride[0]*e+this.stride[1]*r]},i.index=function(t,e){return this.offset+this.stride[0]*t+this.stride[1]*e},i.hi=function(t,e){return new n(this.data,"number"!=typeof t||t<0?this.shape[0]:0|t,"number"!=typeof e||e<0?this.shape[1]:0|e,this.stride[0],this.stride[1],this.offset)},i.lo=function(t,e){var r=this.offset,i=0,a=this.shape[0],o=this.shape[1],s=this.stride[0],l=this.stride[1];return"number"==typeof t&&t>=0&&(r+=s*(i=0|t),a-=i),"number"==typeof e&&e>=0&&(r+=l*(i=0|e),o-=i),new n(this.data,a,o,s,l,r)},i.step=function(t,e){var r=this.shape[0],i=this.shape[1],a=this.stride[0],o=this.stride[1],s=this.offset,l=0,c=Math.ceil;return"number"==typeof t&&((l=0|t)<0?(s+=a*(r-1),r=c(-r/l)):r=c(r/l),a*=l),"number"==typeof e&&((l=0|e)<0?(s+=o*(i-1),i=c(-i/l)):i=c(i/l),o*=l),new n(this.data,r,i,a,o,s)},i.transpose=function(t,e){t=void 0===t?0:0|t,e=void 0===e?1:0|e;var r=this.shape,i=this.stride;return new n(this.data,r[t],r[e],i[t],i[e],this.offset)},i.pick=function(t,r){var n=[],i=[],a=this.offset;return"number"==typeof t&&t>=0?a=a+this.stride[0]*t|0:(n.push(this.shape[0]),i.push(this.stride[0])),"number"==typeof r&&r>=0?a=a+this.stride[1]*r|0:(n.push(this.shape[1]),i.push(this.stride[1])),(0,e[n.length+1])(this.data,n,i,a)},function(t,e,r,i){return new n(t,e[0],e[1],r[0],r[1],i)}},3:function(t,e,r){function n(t,e,r,n,i,a,o,s){this.data=t,this.shape=[e,r,n],this.stride=[i,a,o],this.offset=0|s}var i=n.prototype;return i.dtype=t,i.dimension=3,Object.defineProperty(i,"size",{get:function(){return this.shape[0]*this.shape[1]*this.shape[2]}}),Object.defineProperty(i,"order",{get:function(){var t=Math.abs(this.stride[0]),e=Math.abs(this.stride[1]),r=Math.abs(this.stride[2]);return t>e?e>r?[2,1,0]:t>r?[1,2,0]:[1,0,2]:t>r?[2,0,1]:r>e?[0,1,2]:[0,2,1]}}),i.set=function(e,r,n,i){return"generic"===t?this.data.set(this.offset+this.stride[0]*e+this.stride[1]*r+this.stride[2]*n,i):this.data[this.offset+this.stride[0]*e+this.stride[1]*r+this.stride[2]*n]=i},i.get=function(e,r,n){return"generic"===t?this.data.get(this.offset+this.stride[0]*e+this.stride[1]*r+this.stride[2]*n):this.data[this.offset+this.stride[0]*e+this.stride[1]*r+this.stride[2]*n]},i.index=function(t,e,r){return this.offset+this.stride[0]*t+this.stride[1]*e+this.stride[2]*r},i.hi=function(t,e,r){return new n(this.data,"number"!=typeof t||t<0?this.shape[0]:0|t,"number"!=typeof e||e<0?this.shape[1]:0|e,"number"!=typeof r||r<0?this.shape[2]:0|r,this.stride[0],this.stride[1],this.stride[2],this.offset)},i.lo=function(t,e,r){var i=this.offset,a=0,o=this.shape[0],s=this.shape[1],l=this.shape[2],c=this.stride[0],u=this.stride[1],f=this.stride[2];return"number"==typeof t&&t>=0&&(i+=c*(a=0|t),o-=a),"number"==typeof e&&e>=0&&(i+=u*(a=0|e),s-=a),"number"==typeof r&&r>=0&&(i+=f*(a=0|r),l-=a),new n(this.data,o,s,l,c,u,f,i)},i.step=function(t,e,r){var i=this.shape[0],a=this.shape[1],o=this.shape[2],s=this.stride[0],l=this.stride[1],c=this.stride[2],u=this.offset,f=0,h=Math.ceil;return"number"==typeof t&&((f=0|t)<0?(u+=s*(i-1),i=h(-i/f)):i=h(i/f),s*=f),"number"==typeof e&&((f=0|e)<0?(u+=l*(a-1),a=h(-a/f)):a=h(a/f),l*=f),"number"==typeof r&&((f=0|r)<0?(u+=c*(o-1),o=h(-o/f)):o=h(o/f),c*=f),new n(this.data,i,a,o,s,l,c,u)},i.transpose=function(t,e,r){t=void 0===t?0:0|t,e=void 0===e?1:0|e,r=void 0===r?2:0|r;var i=this.shape,a=this.stride;return new n(this.data,i[t],i[e],i[r],a[t],a[e],a[r],this.offset)},i.pick=function(t,r,n){var i=[],a=[],o=this.offset;return"number"==typeof t&&t>=0?o=o+this.stride[0]*t|0:(i.push(this.shape[0]),a.push(this.stride[0])),"number"==typeof r&&r>=0?o=o+this.stride[1]*r|0:(i.push(this.shape[1]),a.push(this.stride[1])),"number"==typeof n&&n>=0?o=o+this.stride[2]*n|0:(i.push(this.shape[2]),a.push(this.stride[2])),(0,e[i.length+1])(this.data,i,a,o)},function(t,e,r,i){return new n(t,e[0],e[1],e[2],r[0],r[1],r[2],i)}},4:function(t,e,r){function n(t,e,r,n,i,a,o,s,l,c){this.data=t,this.shape=[e,r,n,i],this.stride=[a,o,s,l],this.offset=0|c}var i=n.prototype;return i.dtype=t,i.dimension=4,Object.defineProperty(i,"size",{get:function(){return this.shape[0]*this.shape[1]*this.shape[2]*this.shape[3]}}),Object.defineProperty(i,"order",{get:r}),i.set=function(e,r,n,i,a){return"generic"===t?this.data.set(this.offset+this.stride[0]*e+this.stride[1]*r+this.stride[2]*n+this.stride[3]*i,a):this.data[this.offset+this.stride[0]*e+this.stride[1]*r+this.stride[2]*n+this.stride[3]*i]=a},i.get=function(e,r,n,i){return"generic"===t?this.data.get(this.offset+this.stride[0]*e+this.stride[1]*r+this.stride[2]*n+this.stride[3]*i):this.data[this.offset+this.stride[0]*e+this.stride[1]*r+this.stride[2]*n+this.stride[3]*i]},i.index=function(t,e,r,n){return this.offset+this.stride[0]*t+this.stride[1]*e+this.stride[2]*r+this.stride[3]*n},i.hi=function(t,e,r,i){return new n(this.data,"number"!=typeof t||t<0?this.shape[0]:0|t,"number"!=typeof e||e<0?this.shape[1]:0|e,"number"!=typeof r||r<0?this.shape[2]:0|r,"number"!=typeof i||i<0?this.shape[3]:0|i,this.stride[0],this.stride[1],this.stride[2],this.stride[3],this.offset)},i.lo=function(t,e,r,i){var a=this.offset,o=0,s=this.shape[0],l=this.shape[1],c=this.shape[2],u=this.shape[3],f=this.stride[0],h=this.stride[1],p=this.stride[2],d=this.stride[3];return"number"==typeof t&&t>=0&&(a+=f*(o=0|t),s-=o),"number"==typeof e&&e>=0&&(a+=h*(o=0|e),l-=o),"number"==typeof r&&r>=0&&(a+=p*(o=0|r),c-=o),"number"==typeof i&&i>=0&&(a+=d*(o=0|i),u-=o),new n(this.data,s,l,c,u,f,h,p,d,a)},i.step=function(t,e,r,i){var a=this.shape[0],o=this.shape[1],s=this.shape[2],l=this.shape[3],c=this.stride[0],u=this.stride[1],f=this.stride[2],h=this.stride[3],p=this.offset,d=0,m=Math.ceil;return"number"==typeof t&&((d=0|t)<0?(p+=c*(a-1),a=m(-a/d)):a=m(a/d),c*=d),"number"==typeof e&&((d=0|e)<0?(p+=u*(o-1),o=m(-o/d)):o=m(o/d),u*=d),"number"==typeof r&&((d=0|r)<0?(p+=f*(s-1),s=m(-s/d)):s=m(s/d),f*=d),"number"==typeof i&&((d=0|i)<0?(p+=h*(l-1),l=m(-l/d)):l=m(l/d),h*=d),new n(this.data,a,o,s,l,c,u,f,h,p)},i.transpose=function(t,e,r,i){t=void 0===t?0:0|t,e=void 0===e?1:0|e,r=void 0===r?2:0|r,i=void 0===i?3:0|i;var a=this.shape,o=this.stride;return new n(this.data,a[t],a[e],a[r],a[i],o[t],o[e],o[r],o[i],this.offset)},i.pick=function(t,r,n,i){var a=[],o=[],s=this.offset;return"number"==typeof t&&t>=0?s=s+this.stride[0]*t|0:(a.push(this.shape[0]),o.push(this.stride[0])),"number"==typeof r&&r>=0?s=s+this.stride[1]*r|0:(a.push(this.shape[1]),o.push(this.stride[1])),"number"==typeof n&&n>=0?s=s+this.stride[2]*n|0:(a.push(this.shape[2]),o.push(this.stride[2])),"number"==typeof i&&i>=0?s=s+this.stride[3]*i|0:(a.push(this.shape[3]),o.push(this.stride[3])),(0,e[a.length+1])(this.data,a,o,s)},function(t,e,r,i){return new n(t,e[0],e[1],e[2],e[3],r[0],r[1],r[2],r[3],i)}},5:function(t,e,r){function n(t,e,r,n,i,a,o,s,l,c,u,f){this.data=t,this.shape=[e,r,n,i,a],this.stride=[o,s,l,c,u],this.offset=0|f}var i=n.prototype;return i.dtype=t,i.dimension=5,Object.defineProperty(i,"size",{get:function(){return this.shape[0]*this.shape[1]*this.shape[2]*this.shape[3]*this.shape[4]}}),Object.defineProperty(i,"order",{get:r}),i.set=function(e,r,n,i,a,o){return"generic"===t?this.data.set(this.offset+this.stride[0]*e+this.stride[1]*r+this.stride[2]*n+this.stride[3]*i+this.stride[4]*a,o):this.data[this.offset+this.stride[0]*e+this.stride[1]*r+this.stride[2]*n+this.stride[3]*i+this.stride[4]*a]=o},i.get=function(e,r,n,i,a){return"generic"===t?this.data.get(this.offset+this.stride[0]*e+this.stride[1]*r+this.stride[2]*n+this.stride[3]*i+this.stride[4]*a):this.data[this.offset+this.stride[0]*e+this.stride[1]*r+this.stride[2]*n+this.stride[3]*i+this.stride[4]*a]},i.index=function(t,e,r,n,i){return this.offset+this.stride[0]*t+this.stride[1]*e+this.stride[2]*r+this.stride[3]*n+this.stride[4]*i},i.hi=function(t,e,r,i,a){return new n(this.data,"number"!=typeof t||t<0?this.shape[0]:0|t,"number"!=typeof e||e<0?this.shape[1]:0|e,"number"!=typeof r||r<0?this.shape[2]:0|r,"number"!=typeof i||i<0?this.shape[3]:0|i,"number"!=typeof a||a<0?this.shape[4]:0|a,this.stride[0],this.stride[1],this.stride[2],this.stride[3],this.stride[4],this.offset)},i.lo=function(t,e,r,i,a){var o=this.offset,s=0,l=this.shape[0],c=this.shape[1],u=this.shape[2],f=this.shape[3],h=this.shape[4],p=this.stride[0],d=this.stride[1],m=this.stride[2],g=this.stride[3],v=this.stride[4];return"number"==typeof t&&t>=0&&(o+=p*(s=0|t),l-=s),"number"==typeof e&&e>=0&&(o+=d*(s=0|e),c-=s),"number"==typeof r&&r>=0&&(o+=m*(s=0|r),u-=s),"number"==typeof i&&i>=0&&(o+=g*(s=0|i),f-=s),"number"==typeof a&&a>=0&&(o+=v*(s=0|a),h-=s),new n(this.data,l,c,u,f,h,p,d,m,g,v,o)},i.step=function(t,e,r,i,a){var o=this.shape[0],s=this.shape[1],l=this.shape[2],c=this.shape[3],u=this.shape[4],f=this.stride[0],h=this.stride[1],p=this.stride[2],d=this.stride[3],m=this.stride[4],g=this.offset,v=0,y=Math.ceil;return"number"==typeof t&&((v=0|t)<0?(g+=f*(o-1),o=y(-o/v)):o=y(o/v),f*=v),"number"==typeof e&&((v=0|e)<0?(g+=h*(s-1),s=y(-s/v)):s=y(s/v),h*=v),"number"==typeof r&&((v=0|r)<0?(g+=p*(l-1),l=y(-l/v)):l=y(l/v),p*=v),"number"==typeof i&&((v=0|i)<0?(g+=d*(c-1),c=y(-c/v)):c=y(c/v),d*=v),"number"==typeof a&&((v=0|a)<0?(g+=m*(u-1),u=y(-u/v)):u=y(u/v),m*=v),new n(this.data,o,s,l,c,u,f,h,p,d,m,g)},i.transpose=function(t,e,r,i,a){t=void 0===t?0:0|t,e=void 0===e?1:0|e,r=void 0===r?2:0|r,i=void 0===i?3:0|i,a=void 0===a?4:0|a;var o=this.shape,s=this.stride;return new n(this.data,o[t],o[e],o[r],o[i],o[a],s[t],s[e],s[r],s[i],s[a],this.offset)},i.pick=function(t,r,n,i,a){var o=[],s=[],l=this.offset;return"number"==typeof t&&t>=0?l=l+this.stride[0]*t|0:(o.push(this.shape[0]),s.push(this.stride[0])),"number"==typeof r&&r>=0?l=l+this.stride[1]*r|0:(o.push(this.shape[1]),s.push(this.stride[1])),"number"==typeof n&&n>=0?l=l+this.stride[2]*n|0:(o.push(this.shape[2]),s.push(this.stride[2])),"number"==typeof i&&i>=0?l=l+this.stride[3]*i|0:(o.push(this.shape[3]),s.push(this.stride[3])),"number"==typeof a&&a>=0?l=l+this.stride[4]*a|0:(o.push(this.shape[4]),s.push(this.stride[4])),(0,e[o.length+1])(this.data,o,s,l)},function(t,e,r,i){return new n(t,e[0],e[1],e[2],e[3],e[4],r[0],r[1],r[2],r[3],r[4],i)}}};function l(t,e){var r=-1===e?"T":String(e),n=s[r];return-1===e?n(t):0===e?n(t,c[t][0]):n(t,c[t],o)}var c={generic:[],buffer:[],array:[],float32:[],float64:[],int8:[],int16:[],int32:[],uint8_clamped:[],uint8:[],uint16:[],uint32:[],bigint64:[],biguint64:[]};e.exports=function(t,e,r,a){if(void 0===t)return(0,c.array[0])([]);"number"==typeof t&&(t=[t]),void 0===e&&(e=[t.length]);var o=e.length;if(void 0===r){r=new Array(o);for(var s=o-1,u=1;s>=0;--s)r[s]=u,u*=e[s]}if(void 0===a){a=0;for(s=0;st==t>0?a===-1>>>0?(r+=1,a=0):a+=1:0===a?(a=-1>>>0,r-=1):a-=1;return n.pack(a,r)}},{"double-bits":64}],261:[function(t,e,r){r.vertexNormals=function(t,e,r){for(var n=e.length,i=new Array(n),a=void 0===r?1e-6:r,o=0;oa){var b=i[c],_=1/Math.sqrt(g*y);for(x=0;x<3;++x){var w=(x+1)%3,T=(x+2)%3;b[x]+=_*(v[w]*m[T]-v[T]*m[w])}}}for(o=0;oa)for(_=1/Math.sqrt(k),x=0;x<3;++x)b[x]*=_;else for(x=0;x<3;++x)b[x]=0}return i},r.faceNormals=function(t,e,r){for(var n=t.length,i=new Array(n),a=void 0===r?1e-6:r,o=0;oa?1/Math.sqrt(p):0;for(c=0;c<3;++c)h[c]*=p;i[o]=h}return i}},{}],262:[function(t,e,r){"use strict";e.exports=function(t,e,r,n,i,a,o,s,l,c){var u=e+a+c;if(f>0){var f=Math.sqrt(u+1);t[0]=.5*(o-l)/f,t[1]=.5*(s-n)/f,t[2]=.5*(r-a)/f,t[3]=.5*f}else{var h=Math.max(e,a,c);f=Math.sqrt(2*h-u+1);e>=h?(t[0]=.5*f,t[1]=.5*(i+r)/f,t[2]=.5*(s+n)/f,t[3]=.5*(o-l)/f):a>=h?(t[0]=.5*(r+i)/f,t[1]=.5*f,t[2]=.5*(l+o)/f,t[3]=.5*(s-n)/f):(t[0]=.5*(n+s)/f,t[1]=.5*(o+l)/f,t[2]=.5*f,t[3]=.5*(r-i)/f)}return t}},{}],263:[function(t,e,r){"use strict";e.exports=function(t){var e=(t=t||{}).center||[0,0,0],r=t.rotation||[0,0,0,1],n=t.radius||1;e=[].slice.call(e,0,3),u(r=[].slice.call(r,0,4),r);var i=new f(r,e,Math.log(n));i.setDistanceLimits(t.zoomMin,t.zoomMax),("eye"in t||"up"in t)&&i.lookAt(0,t.eye,t.center,t.up);return i};var n=t("filtered-vector"),i=t("gl-mat4/lookAt"),a=t("gl-mat4/fromQuat"),o=t("gl-mat4/invert"),s=t("./lib/quatFromFrame");function l(t,e,r){return Math.sqrt(Math.pow(t,2)+Math.pow(e,2)+Math.pow(r,2))}function c(t,e,r,n){return Math.sqrt(Math.pow(t,2)+Math.pow(e,2)+Math.pow(r,2)+Math.pow(n,2))}function u(t,e){var r=e[0],n=e[1],i=e[2],a=e[3],o=c(r,n,i,a);o>1e-6?(t[0]=r/o,t[1]=n/o,t[2]=i/o,t[3]=a/o):(t[0]=t[1]=t[2]=0,t[3]=1)}function f(t,e,r){this.radius=n([r]),this.center=n(e),this.rotation=n(t),this.computedRadius=this.radius.curve(0),this.computedCenter=this.center.curve(0),this.computedRotation=this.rotation.curve(0),this.computedUp=[.1,0,0],this.computedEye=[.1,0,0],this.computedMatrix=[.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],this.recalcMatrix(0)}var h=f.prototype;h.lastT=function(){return Math.max(this.radius.lastT(),this.center.lastT(),this.rotation.lastT())},h.recalcMatrix=function(t){this.radius.curve(t),this.center.curve(t),this.rotation.curve(t);var e=this.computedRotation;u(e,e);var r=this.computedMatrix;a(r,e);var n=this.computedCenter,i=this.computedEye,o=this.computedUp,s=Math.exp(this.computedRadius[0]);i[0]=n[0]+s*r[2],i[1]=n[1]+s*r[6],i[2]=n[2]+s*r[10],o[0]=r[1],o[1]=r[5],o[2]=r[9];for(var l=0;l<3;++l){for(var c=0,f=0;f<3;++f)c+=r[l+4*f]*i[f];r[12+l]=-c}},h.getMatrix=function(t,e){this.recalcMatrix(t);var r=this.computedMatrix;if(e){for(var n=0;n<16;++n)e[n]=r[n];return e}return r},h.idle=function(t){this.center.idle(t),this.radius.idle(t),this.rotation.idle(t)},h.flush=function(t){this.center.flush(t),this.radius.flush(t),this.rotation.flush(t)},h.pan=function(t,e,r,n){e=e||0,r=r||0,n=n||0,this.recalcMatrix(t);var i=this.computedMatrix,a=i[1],o=i[5],s=i[9],c=l(a,o,s);a/=c,o/=c,s/=c;var u=i[0],f=i[4],h=i[8],p=u*a+f*o+h*s,d=l(u-=a*p,f-=o*p,h-=s*p);u/=d,f/=d,h/=d;var m=i[2],g=i[6],v=i[10],y=m*a+g*o+v*s,x=m*u+g*f+v*h,b=l(m-=y*a+x*u,g-=y*o+x*f,v-=y*s+x*h);m/=b,g/=b,v/=b;var _=u*e+a*r,w=f*e+o*r,T=h*e+s*r;this.center.move(t,_,w,T);var k=Math.exp(this.computedRadius[0]);k=Math.max(1e-4,k+n),this.radius.set(t,Math.log(k))},h.rotate=function(t,e,r,n){this.recalcMatrix(t),e=e||0,r=r||0;var i=this.computedMatrix,a=i[0],o=i[4],s=i[8],u=i[1],f=i[5],h=i[9],p=i[2],d=i[6],m=i[10],g=e*a+r*u,v=e*o+r*f,y=e*s+r*h,x=-(d*y-m*v),b=-(m*g-p*y),_=-(p*v-d*g),w=Math.sqrt(Math.max(0,1-Math.pow(x,2)-Math.pow(b,2)-Math.pow(_,2))),T=c(x,b,_,w);T>1e-6?(x/=T,b/=T,_/=T,w/=T):(x=b=_=0,w=1);var k=this.computedRotation,A=k[0],M=k[1],S=k[2],E=k[3],L=A*w+E*x+M*_-S*b,C=M*w+E*b+S*x-A*_,P=S*w+E*_+A*b-M*x,I=E*w-A*x-M*b-S*_;if(n){x=p,b=d,_=m;var O=Math.sin(n)/l(x,b,_);x*=O,b*=O,_*=O,I=I*(w=Math.cos(e))-(L=L*w+I*x+C*_-P*b)*x-(C=C*w+I*b+P*x-L*_)*b-(P=P*w+I*_+L*b-C*x)*_}var z=c(L,C,P,I);z>1e-6?(L/=z,C/=z,P/=z,I/=z):(L=C=P=0,I=1),this.rotation.set(t,L,C,P,I)},h.lookAt=function(t,e,r,n){this.recalcMatrix(t),r=r||this.computedCenter,e=e||this.computedEye,n=n||this.computedUp;var a=this.computedMatrix;i(a,e,r,n);var o=this.computedRotation;s(o,a[0],a[1],a[2],a[4],a[5],a[6],a[8],a[9],a[10]),u(o,o),this.rotation.set(t,o[0],o[1],o[2],o[3]);for(var l=0,c=0;c<3;++c)l+=Math.pow(r[c]-e[c],2);this.radius.set(t,.5*Math.log(Math.max(l,1e-6))),this.center.set(t,r[0],r[1],r[2])},h.translate=function(t,e,r,n){this.center.move(t,e||0,r||0,n||0)},h.setMatrix=function(t,e){var r=this.computedRotation;s(r,e[0],e[1],e[2],e[4],e[5],e[6],e[8],e[9],e[10]),u(r,r),this.rotation.set(t,r[0],r[1],r[2],r[3]);var n=this.computedMatrix;o(n,e);var i=n[15];if(Math.abs(i)>1e-6){var a=n[12]/i,l=n[13]/i,c=n[14]/i;this.recalcMatrix(t);var f=Math.exp(this.computedRadius[0]);this.center.set(t,a-n[2]*f,l-n[6]*f,c-n[10]*f),this.radius.idle(t)}else this.center.idle(t),this.radius.idle(t)},h.setDistance=function(t,e){e>0&&this.radius.set(t,Math.log(e))},h.setDistanceLimits=function(t,e){t=t>0?Math.log(t):-1/0,e=e>0?Math.log(e):1/0,e=Math.max(e,t),this.radius.bounds[0][0]=t,this.radius.bounds[1][0]=e},h.getDistanceLimits=function(t){var e=this.radius.bounds;return t?(t[0]=Math.exp(e[0][0]),t[1]=Math.exp(e[1][0]),t):[Math.exp(e[0][0]),Math.exp(e[1][0])]},h.toJSON=function(){return this.recalcMatrix(this.lastT()),{center:this.computedCenter.slice(),rotation:this.computedRotation.slice(),distance:Math.log(this.computedRadius[0]),zoomMin:this.radius.bounds[0][0],zoomMax:this.radius.bounds[1][0]}},h.fromJSON=function(t){var e=this.lastT(),r=t.center;r&&this.center.set(e,r[0],r[1],r[2]);var n=t.rotation;n&&this.rotation.set(e,n[0],n[1],n[2],n[3]);var i=t.distance;i&&i>0&&this.radius.set(e,Math.log(i)),this.setDistanceLimits(t.zoomMin,t.zoomMax)}},{"./lib/quatFromFrame":262,"filtered-vector":68,"gl-mat4/fromQuat":95,"gl-mat4/invert":98,"gl-mat4/lookAt":99}],264:[function(t,e,r){ +/*! + * pad-left + * + * Copyright (c) 2014-2015, Jon Schlinkert. + * Licensed under the MIT license. + */ +"use strict";var n=t("repeat-string");e.exports=function(t,e,r){return n(r=void 0!==r?r+"":" ",e)+t}},{"repeat-string":277}],265:[function(t,e,r){e.exports=function(t,e){e||(e=[0,""]),t=String(t);var r=parseFloat(t,10);return e[0]=r,e[1]=t.match(/[\d.\-\+]*\s*(.*)/)[1]||"",e}},{}],266:[function(t,e,r){"use strict";e.exports=function(t,e){for(var r=0|e.length,i=t.length,a=[new Array(r),new Array(r)],o=0;o0){o=a[u][r][0],l=u;break}s=o[1^l];for(var f=0;f<2;++f)for(var h=a[f][r],p=0;p0&&(o=d,s=m,l=f)}return i||o&&c(o,l),s}function f(t,r){var i=a[r][t][0],o=[t];c(i,r);for(var s=i[1^r];;){for(;s!==t;)o.push(s),s=u(o[o.length-2],s,!1);if(a[0][t].length+a[1][t].length===0)break;var l=o[o.length-1],f=t,h=o[1],p=u(l,f,!0);if(n(e[l],e[f],e[h],e[p])<0)break;o.push(t),s=u(l,f)}return o}function h(t,e){return e[1]===e[e.length-1]}for(o=0;o0;){a[0][o].length;var m=f(o,p);h(0,m)?d.push.apply(d,m):(d.length>0&&l.push(d),d=m)}d.length>0&&l.push(d)}return l};var n=t("compare-angle")},{"compare-angle":54}],267:[function(t,e,r){"use strict";e.exports=function(t,e){for(var r=n(t,e.length),i=new Array(e.length),a=new Array(e.length),o=[],s=0;s0;){var c=o.pop();i[c]=!1;var u=r[c];for(s=0;s0}))).length,g=new Array(m),v=new Array(m);for(p=0;p0;){var B=R.pop(),N=E[B];l(N,(function(t,e){return t-e}));var j,U=N.length,V=F[B];if(0===V){var H=d[B];j=[H]}for(p=0;p=0))if(F[q]=1^V,R.push(q),0===V)D(H=d[q])||(H.reverse(),j.push(H))}0===V&&r.push(j)}return r};var n=t("edges-to-adjacency-list"),i=t("planar-dual"),a=t("point-in-big-polygon"),o=t("two-product"),s=t("robust-sum"),l=t("uniq"),c=t("./lib/trim-leaves");function u(t,e){for(var r=new Array(t),n=0;n0&&e[i]===r[0]))return 1;a=t[i-1]}for(var s=1;a;){var l=a.key,c=n(r,l[0],l[1]);if(l[0][0]0))return 0;s=-1,a=a.right}else if(c>0)a=a.left;else{if(!(c<0))return 0;s=1,a=a.right}}return s}}(v.slabs,v.coordinates);return 0===a.length?y:function(t,e){return function(r){return t(r[0],r[1])?0:e(r)}}(l(a),y)};var n=t("robust-orientation")[3],i=t("slab-decomposition"),a=t("interval-tree-1d"),o=t("binary-search-bounds");function s(){return!0}function l(t){for(var e={},r=0;r=c?(k=1,y=c+2*h+d):y=h*(k=-h/c)+d):(k=0,p>=0?(A=0,y=d):-p>=f?(A=1,y=f+2*p+d):y=p*(A=-p/f)+d);else if(A<0)A=0,h>=0?(k=0,y=d):-h>=c?(k=1,y=c+2*h+d):y=h*(k=-h/c)+d;else{var M=1/T;y=(k*=M)*(c*k+u*(A*=M)+2*h)+A*(u*k+f*A+2*p)+d}else k<0?(b=f+p)>(x=u+h)?(_=b-x)>=(w=c-2*u+f)?(k=1,A=0,y=c+2*h+d):y=(k=_/w)*(c*k+u*(A=1-k)+2*h)+A*(u*k+f*A+2*p)+d:(k=0,b<=0?(A=1,y=f+2*p+d):p>=0?(A=0,y=d):y=p*(A=-p/f)+d):A<0?(b=c+h)>(x=u+p)?(_=b-x)>=(w=c-2*u+f)?(A=1,k=0,y=f+2*p+d):y=(k=1-(A=_/w))*(c*k+u*A+2*h)+A*(u*k+f*A+2*p)+d:(A=0,b<=0?(k=1,y=c+2*h+d):h>=0?(k=0,y=d):y=h*(k=-h/c)+d):(_=f+p-u-h)<=0?(k=0,A=1,y=f+2*p+d):_>=(w=c-2*u+f)?(k=1,A=0,y=c+2*h+d):y=(k=_/w)*(c*k+u*(A=1-k)+2*h)+A*(u*k+f*A+2*p)+d;var S=1-k-A;for(l=0;l0){var c=t[r-1];if(0===n(s,c)&&a(c)!==l){r-=1;continue}}t[r++]=s}}return t.length=r,t}},{"cell-orientation":47,"compare-cell":56,"compare-oriented-cell":57}],277:[function(t,e,r){ +/*! + * repeat-string + * + * Copyright (c) 2014-2015, Jon Schlinkert. + * Licensed under the MIT License. + */ +"use strict";var n,i="";e.exports=function(t,e){if("string"!=typeof t)throw new TypeError("expected a string");if(1===e)return t;if(2===e)return t+t;var r=t.length*e;if(n!==t||void 0===n)n=t,i="";else if(i.length>=r)return i.substr(0,r);for(;r>i.length&&e>1;)1&e&&(i+=t),e>>=1,t+=t;return i=(i+=t).substr(0,r)}},{}],278:[function(t,e,r){(function(t){(function(){e.exports=t.performance&&t.performance.now?function(){return performance.now()}:Date.now||function(){return+new Date}}).call(this)}).call(this,void 0!==n?n:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],279:[function(t,e,r){"use strict";e.exports=function(t){for(var e=t.length,r=t[t.length-1],n=e,i=e-2;i>=0;--i){var a=r,o=t[i];(l=o-((r=a+o)-a))&&(t[--n]=r,r=l)}var s=0;for(i=n;i0){if(a<=0)return o;n=i+a}else{if(!(i<0))return o;if(a>=0)return o;n=-(i+a)}var s=33306690738754716e-32*n;return o>=s||o<=-s?o:f(t,e,r)},function(t,e,r,n){var i=t[0]-n[0],a=e[0]-n[0],o=r[0]-n[0],s=t[1]-n[1],l=e[1]-n[1],c=r[1]-n[1],u=t[2]-n[2],f=e[2]-n[2],p=r[2]-n[2],d=a*c,m=o*l,g=o*s,v=i*c,y=i*l,x=a*s,b=u*(d-m)+f*(g-v)+p*(y-x),_=7771561172376103e-31*((Math.abs(d)+Math.abs(m))*Math.abs(u)+(Math.abs(g)+Math.abs(v))*Math.abs(f)+(Math.abs(y)+Math.abs(x))*Math.abs(p));return b>_||-b>_?b:h(t,e,r,n)}];function d(t){var e=p[t.length];return e||(e=p[t.length]=u(t.length)),e.apply(void 0,t)}function m(t,e,r,n,i,a,o){return function(e,r,s,l,c){switch(arguments.length){case 0:case 1:return 0;case 2:return n(e,r);case 3:return i(e,r,s);case 4:return a(e,r,s,l);case 5:return o(e,r,s,l,c)}for(var u=new Array(arguments.length),f=0;f0&&o>0||a<0&&o<0)return!1;var s=n(r,t,e),l=n(i,t,e);if(s>0&&l>0||s<0&&l<0)return!1;if(0===a&&0===o&&0===s&&0===l)return function(t,e,r,n){for(var i=0;i<2;++i){var a=t[i],o=e[i],s=Math.min(a,o),l=Math.max(a,o),c=r[i],u=n[i],f=Math.min(c,u);if(Math.max(c,u)=n?(i=f,(l+=1)=n?(i=f,(l+=1)>1,c=e[2*l+1];if(c===a)return l;a>1,c=e[2*l+1];if(c===a)return l;a>1,c=e[2*l+1];if(c===a)return l;a>1,s=a(t[o],e);s<=0?(0===s&&(i=o),r=o+1):s>0&&(n=o-1)}return i}function u(t,e){for(var r=new Array(t.length),i=0,o=r.length;i=t.length||0!==a(t[g],s)););}return r}function f(t,e){if(e<0)return[];for(var r=[],i=(1<>>u&1&&c.push(i[u]);e.push(c)}return s(e)},r.skeleton=f,r.boundary=function(t){for(var e=[],r=0,n=t.length;r>1:(t>>1)-1}function x(t){for(var e=v(t);;){var r=e,n=2*t+1,i=2*(t+1),a=t;if(n0;){var r=y(t);if(r>=0)if(e0){var t=k[0];return g(0,M-1),M-=1,x(0),t}return-1}function w(t,e){var r=k[t];return c[r]===e?t:(c[r]=-1/0,b(t),_(),c[r]=e,b((M+=1)-1))}function T(t){if(!u[t]){u[t]=!0;var e=s[t],r=l[t];s[r]>=0&&(s[r]=e),l[e]>=0&&(l[e]=r),A[e]>=0&&w(A[e],m(e)),A[r]>=0&&w(A[r],m(r))}}var k=[],A=new Array(a);for(f=0;f>1;f>=0;--f)x(f);for(;;){var S=_();if(S<0||c[S]>r)break;T(S)}var E=[];for(f=0;f=0&&r>=0&&e!==r){var n=A[e],i=A[r];n!==i&&C.push([n,i])}})),i.unique(i.normalize(C)),{positions:E,edges:C}};var n=t("robust-orientation"),i=t("simplicial-complex")},{"robust-orientation":284,"simplicial-complex":295}],298:[function(t,e,r){"use strict";e.exports=function(t,e){var r,a,o,s;if(e[0][0]e[1][0]))return i(e,t);r=e[1],a=e[0]}if(t[0][0]t[1][0]))return-i(t,e);o=t[1],s=t[0]}var l=n(r,a,s),c=n(r,a,o);if(l<0){if(c<=0)return l}else if(l>0){if(c>=0)return l}else if(c)return c;if(l=n(s,o,a),c=n(s,o,r),l<0){if(c<=0)return l}else if(l>0){if(c>=0)return l}else if(c)return c;return a[0]-s[0]};var n=t("robust-orientation");function i(t,e){var r,i,a,o;if(e[0][0]e[1][0])){var s=Math.min(t[0][1],t[1][1]),l=Math.max(t[0][1],t[1][1]),c=Math.min(e[0][1],e[1][1]),u=Math.max(e[0][1],e[1][1]);return lu?s-u:l-u}r=e[1],i=e[0]}t[0][1]0)if(e[0]!==o[1][0])r=t,t=t.right;else{if(l=c(t.right,e))return l;t=t.left}else{if(e[0]!==o[1][0])return t;var l;if(l=c(t.right,e))return l;t=t.left}}return r}function u(t,e,r,n){this.y=t,this.index=e,this.start=r,this.closed=n}function f(t,e,r,n){this.x=t,this.segment=e,this.create=r,this.index=n}s.prototype.castUp=function(t){var e=n.le(this.coordinates,t[0]);if(e<0)return-1;this.slabs[e];var r=c(this.slabs[e],t),i=-1;if(r&&(i=r.value),this.coordinates[e]===t[0]){var s=null;if(r&&(s=r.key),e>0){var u=c(this.slabs[e-1],t);u&&(s?o(u.key,s)>0&&(s=u.key,i=u.value):(i=u.value,s=u.key))}var f=this.horizontal[e];if(f.length>0){var h=n.ge(f,t[1],l);if(h=f.length)return i;p=f[h]}}if(p.start)if(s){var d=a(s[0],s[1],[t[0],p.y]);s[0][0]>s[1][0]&&(d=-d),d>0&&(i=p.index)}else i=p.index;else p.y!==t[1]&&(i=p.index)}}}return i}},{"./lib/order-segments":298,"binary-search-bounds":31,"functional-red-black-tree":69,"robust-orientation":284}],300:[function(t,e,r){"use strict";var n=t("robust-dot-product"),i=t("robust-sum");function a(t,e){var r=i(n(t,e),[e[e.length-1]]);return r[r.length-1]}function o(t,e,r,n){var i=-e/(n-e);i<0?i=0:i>1&&(i=1);for(var a=1-i,o=t.length,s=new Array(o),l=0;l0||i>0&&u<0){var f=o(s,u,l,i);r.push(f),n.push(f.slice())}u<0?n.push(l.slice()):u>0?r.push(l.slice()):(r.push(l.slice()),n.push(l.slice())),i=u}return{positive:r,negative:n}},e.exports.positive=function(t,e){for(var r=[],n=a(t[t.length-1],e),i=t[t.length-1],s=t[0],l=0;l0||n>0&&c<0)&&r.push(o(i,c,s,n)),c>=0&&r.push(s.slice()),n=c}return r},e.exports.negative=function(t,e){for(var r=[],n=a(t[t.length-1],e),i=t[t.length-1],s=t[0],l=0;l0||n>0&&c<0)&&r.push(o(i,c,s,n)),c<=0&&r.push(s.slice()),n=c}return r}},{"robust-dot-product":281,"robust-sum":289}],301:[function(t,e,r){!function(){"use strict";var t={not_string:/[^s]/,not_bool:/[^t]/,not_type:/[^T]/,not_primitive:/[^v]/,number:/[diefg]/,numeric_arg:/[bcdiefguxX]/,json:/[j]/,not_json:/[^j]/,text:/^[^\x25]+/,modulo:/^\x25{2}/,placeholder:/^\x25(?:([1-9]\d*)\$|\(([^)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-gijostTuvxX])/,key:/^([a-z_][a-z_\d]*)/i,key_access:/^\.([a-z_][a-z_\d]*)/i,index_access:/^\[(\d+)\]/,sign:/^[+-]/};function e(t){return i(o(t),arguments)}function n(t,r){return e.apply(null,[t].concat(r||[]))}function i(r,n){var i,a,o,s,l,c,u,f,h,p=1,d=r.length,m="";for(a=0;a=0),s.type){case"b":i=parseInt(i,10).toString(2);break;case"c":i=String.fromCharCode(parseInt(i,10));break;case"d":case"i":i=parseInt(i,10);break;case"j":i=JSON.stringify(i,null,s.width?parseInt(s.width):0);break;case"e":i=s.precision?parseFloat(i).toExponential(s.precision):parseFloat(i).toExponential();break;case"f":i=s.precision?parseFloat(i).toFixed(s.precision):parseFloat(i);break;case"g":i=s.precision?String(Number(i.toPrecision(s.precision))):parseFloat(i);break;case"o":i=(parseInt(i,10)>>>0).toString(8);break;case"s":i=String(i),i=s.precision?i.substring(0,s.precision):i;break;case"t":i=String(!!i),i=s.precision?i.substring(0,s.precision):i;break;case"T":i=Object.prototype.toString.call(i).slice(8,-1).toLowerCase(),i=s.precision?i.substring(0,s.precision):i;break;case"u":i=parseInt(i,10)>>>0;break;case"v":i=i.valueOf(),i=s.precision?i.substring(0,s.precision):i;break;case"x":i=(parseInt(i,10)>>>0).toString(16);break;case"X":i=(parseInt(i,10)>>>0).toString(16).toUpperCase()}t.json.test(s.type)?m+=i:(!t.number.test(s.type)||f&&!s.sign?h="":(h=f?"+":"-",i=i.toString().replace(t.sign,"")),c=s.pad_char?"0"===s.pad_char?"0":s.pad_char.charAt(1):" ",u=s.width-(h+i).length,l=s.width&&u>0?c.repeat(u):"",m+=s.align?h+i+l:"0"===c?h+l+i:l+h+i)}return m}var a=Object.create(null);function o(e){if(a[e])return a[e];for(var r,n=e,i=[],o=0;n;){if(null!==(r=t.text.exec(n)))i.push(r[0]);else if(null!==(r=t.modulo.exec(n)))i.push("%");else{if(null===(r=t.placeholder.exec(n)))throw new SyntaxError("[sprintf] unexpected placeholder");if(r[2]){o|=1;var s=[],l=r[2],c=[];if(null===(c=t.key.exec(l)))throw new SyntaxError("[sprintf] failed to parse named argument key");for(s.push(c[1]);""!==(l=l.substring(c[0].length));)if(null!==(c=t.key_access.exec(l)))s.push(c[1]);else{if(null===(c=t.index_access.exec(l)))throw new SyntaxError("[sprintf] failed to parse named argument key");s.push(c[1])}r[2]=s}else o|=2;if(3===o)throw new Error("[sprintf] mixing positional and named placeholders is not (yet) supported");i.push({placeholder:r[0],param_no:r[1],keys:r[2],sign:r[3],pad_char:r[4],align:r[5],width:r[6],precision:r[7],type:r[8]})}n=n.substring(r[0].length)}return a[e]=i}void 0!==r&&(r.sprintf=e,r.vsprintf=n),"undefined"!=typeof window&&(window.sprintf=e,window.vsprintf=n)}()},{}],302:[function(t,e,r){"use strict";e.exports=function(t,e){if(t.dimension<=0)return{positions:[],cells:[]};if(1===t.dimension)return function(t,e){for(var r=i(t,e),n=r.length,a=new Array(n),o=new Array(n),s=0;sn|0},vertex:function(t,e,r,n,i,a,o,s,l,c,u,f,h){var p=(o<<0)+(s<<1)+(l<<2)+(c<<3)|0;if(0!==p&&15!==p)switch(p){case 0:u.push([t-.5,e-.5]);break;case 1:u.push([t-.25-.25*(n+r-2*h)/(r-n),e-.25-.25*(i+r-2*h)/(r-i)]);break;case 2:u.push([t-.75-.25*(-n-r+2*h)/(n-r),e-.25-.25*(a+n-2*h)/(n-a)]);break;case 3:u.push([t-.5,e-.5-.5*(i+r+a+n-4*h)/(r-i+n-a)]);break;case 4:u.push([t-.25-.25*(a+i-2*h)/(i-a),e-.75-.25*(-i-r+2*h)/(i-r)]);break;case 5:u.push([t-.5-.5*(n+r+a+i-4*h)/(r-n+i-a),e-.5]);break;case 6:u.push([t-.5-.25*(-n-r+a+i)/(n-r+i-a),e-.5-.25*(-i-r+a+n)/(i-r+n-a)]);break;case 7:u.push([t-.75-.25*(a+i-2*h)/(i-a),e-.75-.25*(a+n-2*h)/(n-a)]);break;case 8:u.push([t-.75-.25*(-a-i+2*h)/(a-i),e-.75-.25*(-a-n+2*h)/(a-n)]);break;case 9:u.push([t-.5-.25*(n+r+-a-i)/(r-n+a-i),e-.5-.25*(i+r+-a-n)/(r-i+a-n)]);break;case 10:u.push([t-.5-.5*(-n-r-a-i+4*h)/(n-r+a-i),e-.5]);break;case 11:u.push([t-.25-.25*(-a-i+2*h)/(a-i),e-.75-.25*(i+r-2*h)/(r-i)]);break;case 12:u.push([t-.5,e-.5-.5*(-i-r-a-n+4*h)/(i-r+a-n)]);break;case 13:u.push([t-.75-.25*(n+r-2*h)/(r-n),e-.25-.25*(-a-n+2*h)/(a-n)]);break;case 14:u.push([t-.25-.25*(-n-r+2*h)/(n-r),e-.25-.25*(-i-r+2*h)/(i-r)]);break;case 15:u.push([t-.5,e-.5])}},cell:function(t,e,r,n,i,a,o,s,l){i?s.push([t,e]):s.push([e,t])}});return function(t,e){var r=[],i=[];return n(t,r,i,e),{positions:r,cells:i}}}};var o={}},{"ndarray-extract-contour":251,"zero-crossings":318}],303:[function(t,e,r){(function(r){(function(){"use strict";e.exports=function t(e,r,i){i=i||{};var o=a[e];o||(o=a[e]={" ":{data:new Float32Array(0),shape:.2}});var s=o[r];if(!s)if(r.length<=1||!/\d/.test(r))s=o[r]=function(t){for(var e=t.cells,r=t.positions,n=new Float32Array(6*e.length),i=0,a=0,o=0;o0&&(f+=.02);var p=new Float32Array(u),d=0,m=-.5*f;for(h=0;hMath.max(r,n)?i[2]=1:r>Math.max(e,n)?i[0]=1:i[1]=1;for(var a=0,o=0,l=0;l<3;++l)a+=t[l]*t[l],o+=i[l]*t[l];for(l=0;l<3;++l)i[l]-=o/a*t[l];return s(i,i),i}function h(t,e,r,i,a,o,s,l){this.center=n(r),this.up=n(i),this.right=n(a),this.radius=n([o]),this.angle=n([s,l]),this.angle.bounds=[[-1/0,-Math.PI/2],[1/0,Math.PI/2]],this.setDistanceLimits(t,e),this.computedCenter=this.center.curve(0),this.computedUp=this.up.curve(0),this.computedRight=this.right.curve(0),this.computedRadius=this.radius.curve(0),this.computedAngle=this.angle.curve(0),this.computedToward=[0,0,0],this.computedEye=[0,0,0],this.computedMatrix=new Array(16);for(var c=0;c<16;++c)this.computedMatrix[c]=.5;this.recalcMatrix(0)}var p=h.prototype;p.setDistanceLimits=function(t,e){t=t>0?Math.log(t):-1/0,e=e>0?Math.log(e):1/0,e=Math.max(e,t),this.radius.bounds[0][0]=t,this.radius.bounds[1][0]=e},p.getDistanceLimits=function(t){var e=this.radius.bounds[0];return t?(t[0]=Math.exp(e[0][0]),t[1]=Math.exp(e[1][0]),t):[Math.exp(e[0][0]),Math.exp(e[1][0])]},p.recalcMatrix=function(t){this.center.curve(t),this.up.curve(t),this.right.curve(t),this.radius.curve(t),this.angle.curve(t);for(var e=this.computedUp,r=this.computedRight,n=0,i=0,a=0;a<3;++a)i+=e[a]*r[a],n+=e[a]*e[a];var l=Math.sqrt(n),u=0;for(a=0;a<3;++a)r[a]-=e[a]*i/n,u+=r[a]*r[a],e[a]/=l;var f=Math.sqrt(u);for(a=0;a<3;++a)r[a]/=f;var h=this.computedToward;o(h,e,r),s(h,h);var p=Math.exp(this.computedRadius[0]),d=this.computedAngle[0],m=this.computedAngle[1],g=Math.cos(d),v=Math.sin(d),y=Math.cos(m),x=Math.sin(m),b=this.computedCenter,_=g*y,w=v*y,T=x,k=-g*x,A=-v*x,M=y,S=this.computedEye,E=this.computedMatrix;for(a=0;a<3;++a){var L=_*r[a]+w*h[a]+T*e[a];E[4*a+1]=k*r[a]+A*h[a]+M*e[a],E[4*a+2]=L,E[4*a+3]=0}var C=E[1],P=E[5],I=E[9],O=E[2],z=E[6],D=E[10],R=P*D-I*z,F=I*O-C*D,B=C*z-P*O,N=c(R,F,B);R/=N,F/=N,B/=N,E[0]=R,E[4]=F,E[8]=B;for(a=0;a<3;++a)S[a]=b[a]+E[2+4*a]*p;for(a=0;a<3;++a){u=0;for(var j=0;j<3;++j)u+=E[a+4*j]*S[j];E[12+a]=-u}E[15]=1},p.getMatrix=function(t,e){this.recalcMatrix(t);var r=this.computedMatrix;if(e){for(var n=0;n<16;++n)e[n]=r[n];return e}return r};var d=[0,0,0];p.rotate=function(t,e,r,n){if(this.angle.move(t,e,r),n){this.recalcMatrix(t);var i=this.computedMatrix;d[0]=i[2],d[1]=i[6],d[2]=i[10];for(var o=this.computedUp,s=this.computedRight,l=this.computedToward,c=0;c<3;++c)i[4*c]=o[c],i[4*c+1]=s[c],i[4*c+2]=l[c];a(i,i,n,d);for(c=0;c<3;++c)o[c]=i[4*c],s[c]=i[4*c+1];this.up.set(t,o[0],o[1],o[2]),this.right.set(t,s[0],s[1],s[2])}},p.pan=function(t,e,r,n){e=e||0,r=r||0,n=n||0,this.recalcMatrix(t);var i=this.computedMatrix,a=(Math.exp(this.computedRadius[0]),i[1]),o=i[5],s=i[9],l=c(a,o,s);a/=l,o/=l,s/=l;var u=i[0],f=i[4],h=i[8],p=u*a+f*o+h*s,d=c(u-=a*p,f-=o*p,h-=s*p),m=(u/=d)*e+a*r,g=(f/=d)*e+o*r,v=(h/=d)*e+s*r;this.center.move(t,m,g,v);var y=Math.exp(this.computedRadius[0]);y=Math.max(1e-4,y+n),this.radius.set(t,Math.log(y))},p.translate=function(t,e,r,n){this.center.move(t,e||0,r||0,n||0)},p.setMatrix=function(t,e,r,n){var a=1;"number"==typeof r&&(a=0|r),(a<0||a>3)&&(a=1);var o=(a+2)%3;e||(this.recalcMatrix(t),e=this.computedMatrix);var s=e[a],l=e[a+4],f=e[a+8];if(n){var h=Math.abs(s),p=Math.abs(l),d=Math.abs(f),m=Math.max(h,p,d);h===m?(s=s<0?-1:1,l=f=0):d===m?(f=f<0?-1:1,s=l=0):(l=l<0?-1:1,s=f=0)}else{var g=c(s,l,f);s/=g,l/=g,f/=g}var v,y,x=e[o],b=e[o+4],_=e[o+8],w=x*s+b*l+_*f,T=c(x-=s*w,b-=l*w,_-=f*w),k=l*(_/=T)-f*(b/=T),A=f*(x/=T)-s*_,M=s*b-l*x,S=c(k,A,M);if(k/=S,A/=S,M/=S,this.center.jump(t,q,G,Y),this.radius.idle(t),this.up.jump(t,s,l,f),this.right.jump(t,x,b,_),2===a){var E=e[1],L=e[5],C=e[9],P=E*x+L*b+C*_,I=E*k+L*A+C*M;v=R<0?-Math.PI/2:Math.PI/2,y=Math.atan2(I,P)}else{var O=e[2],z=e[6],D=e[10],R=O*s+z*l+D*f,F=O*x+z*b+D*_,B=O*k+z*A+D*M;v=Math.asin(u(R)),y=Math.atan2(B,F)}this.angle.jump(t,y,v),this.recalcMatrix(t);var N=e[2],j=e[6],U=e[10],V=this.computedMatrix;i(V,e);var H=V[15],q=V[12]/H,G=V[13]/H,Y=V[14]/H,W=Math.exp(this.computedRadius[0]);this.center.jump(t,q-N*W,G-j*W,Y-U*W)},p.lastT=function(){return Math.max(this.center.lastT(),this.up.lastT(),this.right.lastT(),this.radius.lastT(),this.angle.lastT())},p.idle=function(t){this.center.idle(t),this.up.idle(t),this.right.idle(t),this.radius.idle(t),this.angle.idle(t)},p.flush=function(t){this.center.flush(t),this.up.flush(t),this.right.flush(t),this.radius.flush(t),this.angle.flush(t)},p.setDistance=function(t,e){e>0&&this.radius.set(t,Math.log(e))},p.lookAt=function(t,e,r,n){this.recalcMatrix(t),e=e||this.computedEye,r=r||this.computedCenter;var i=(n=n||this.computedUp)[0],a=n[1],o=n[2],s=c(i,a,o);if(!(s<1e-6)){i/=s,a/=s,o/=s;var l=e[0]-r[0],f=e[1]-r[1],h=e[2]-r[2],p=c(l,f,h);if(!(p<1e-6)){l/=p,f/=p,h/=p;var d=this.computedRight,m=d[0],g=d[1],v=d[2],y=i*m+a*g+o*v,x=c(m-=y*i,g-=y*a,v-=y*o);if(!(x<.01&&(x=c(m=a*h-o*f,g=o*l-i*h,v=i*f-a*l))<1e-6)){m/=x,g/=x,v/=x,this.up.set(t,i,a,o),this.right.set(t,m,g,v),this.center.set(t,r[0],r[1],r[2]),this.radius.set(t,Math.log(p));var b=a*v-o*g,_=o*m-i*v,w=i*g-a*m,T=c(b,_,w),k=i*l+a*f+o*h,A=m*l+g*f+v*h,M=(b/=T)*l+(_/=T)*f+(w/=T)*h,S=Math.asin(u(k)),E=Math.atan2(M,A),L=this.angle._state,C=L[L.length-1],P=L[L.length-2];C%=2*Math.PI;var I=Math.abs(C+2*Math.PI-E),O=Math.abs(C-E),z=Math.abs(C-2*Math.PI-E);I0?r.pop():new ArrayBuffer(t)}function d(t){return new Uint8Array(p(t),0,t)}function m(t){return new Uint16Array(p(2*t),0,t)}function g(t){return new Uint32Array(p(4*t),0,t)}function v(t){return new Int8Array(p(t),0,t)}function y(t){return new Int16Array(p(2*t),0,t)}function x(t){return new Int32Array(p(4*t),0,t)}function b(t){return new Float32Array(p(4*t),0,t)}function _(t){return new Float64Array(p(8*t),0,t)}function w(t){return o?new Uint8ClampedArray(p(t),0,t):d(t)}function T(t){return s?new BigUint64Array(p(8*t),0,t):null}function k(t){return l?new BigInt64Array(p(8*t),0,t):null}function A(t){return new DataView(p(t),0,t)}function M(t){t=n.nextPow2(t);var e=n.log2(t),r=f[e];return r.length>0?r.pop():new a(t)}r.free=function(t){if(a.isBuffer(t))f[n.log2(t.length)].push(t);else{if("[object ArrayBuffer]"!==Object.prototype.toString.call(t)&&(t=t.buffer),!t)return;var e=t.length||t.byteLength,r=0|n.log2(e);u[r].push(t)}},r.freeUint8=r.freeUint16=r.freeUint32=r.freeBigUint64=r.freeInt8=r.freeInt16=r.freeInt32=r.freeBigInt64=r.freeFloat32=r.freeFloat=r.freeFloat64=r.freeDouble=r.freeUint8Clamped=r.freeDataView=function(t){h(t.buffer)},r.freeArrayBuffer=h,r.freeBuffer=function(t){f[n.log2(t.length)].push(t)},r.malloc=function(t,e){if(void 0===e||"arraybuffer"===e)return p(t);switch(e){case"uint8":return d(t);case"uint16":return m(t);case"uint32":return g(t);case"int8":return v(t);case"int16":return y(t);case"int32":return x(t);case"float":case"float32":return b(t);case"double":case"float64":return _(t);case"uint8_clamped":return w(t);case"bigint64":return k(t);case"biguint64":return T(t);case"buffer":return M(t);case"data":case"dataview":return A(t);default:return null}return null},r.mallocArrayBuffer=p,r.mallocUint8=d,r.mallocUint16=m,r.mallocUint32=g,r.mallocInt8=v,r.mallocInt16=y,r.mallocInt32=x,r.mallocFloat32=r.mallocFloat=b,r.mallocFloat64=r.mallocDouble=_,r.mallocUint8Clamped=w,r.mallocBigUint64=T,r.mallocBigInt64=k,r.mallocDataView=A,r.mallocBuffer=M,r.clearCache=function(){for(var t=0;t<32;++t)c.UINT8[t].length=0,c.UINT16[t].length=0,c.UINT32[t].length=0,c.INT8[t].length=0,c.INT16[t].length=0,c.INT32[t].length=0,c.FLOAT[t].length=0,c.DOUBLE[t].length=0,c.BIGUINT64[t].length=0,c.BIGINT64[t].length=0,c.UINT8C[t].length=0,u[t].length=0,f[t].length=0}}).call(this)}).call(this,void 0!==n?n:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{"bit-twiddle":32,buffer:3,dup:65}],309:[function(t,e,r){"use strict";function n(t){this.roots=new Array(t),this.ranks=new Array(t);for(var e=0;e0&&(a=n.size),n.lineSpacing&&n.lineSpacing>0&&(o=n.lineSpacing),n.styletags&&n.styletags.breaklines&&(s.breaklines=!!n.styletags.breaklines),n.styletags&&n.styletags.bolds&&(s.bolds=!!n.styletags.bolds),n.styletags&&n.styletags.italics&&(s.italics=!!n.styletags.italics),n.styletags&&n.styletags.subscripts&&(s.subscripts=!!n.styletags.subscripts),n.styletags&&n.styletags.superscripts&&(s.superscripts=!!n.styletags.superscripts));return r.font=[n.fontStyle,n.fontVariant,n.fontWeight,a+"px",n.font].filter((function(t){return t})).join(" "),r.textAlign="start",r.textBaseline="alphabetic",r.direction="ltr",h(function(t,e,r,n,a,o){r=r.replace(/\n/g,""),r=!0===o.breaklines?r.replace(/\/g,"\n"):r.replace(/\/g," ");var s="",l=[];for(p=0;p-1?parseInt(t[1+i]):0,l=a>-1?parseInt(r[1+a]):0;s!==l&&(n=n.replace(S(),"?px "),g*=Math.pow(.75,l-s),n=n.replace("?px ",S())),m+=.25*x*(l-s)}if(!0===o.superscripts){var c=t.indexOf("+"),u=r.indexOf("+"),f=c>-1?parseInt(t[1+c]):0,h=u>-1?parseInt(r[1+u]):0;f!==h&&(n=n.replace(S(),"?px "),g*=Math.pow(.75,h-f),n=n.replace("?px ",S())),m-=.25*x*(h-f)}if(!0===o.bolds){var p=t.indexOf("b|")>-1,d=r.indexOf("b|")>-1;!p&&d&&(n=v?n.replace("italic ","italic bold "):"bold "+n),p&&!d&&(n=n.replace("bold ",""))}if(!0===o.italics){var v=t.indexOf("i|")>-1,y=r.indexOf("i|")>-1;!v&&y&&(n="italic "+n),v&&!y&&(n=n.replace("italic ",""))}e.font=n}for(h=0;h",a="",o=i.length,s=a.length,l="+"===e[0]||"-"===e[0],c=0,u=-s;c>-1&&-1!==(c=r.indexOf(i,c))&&-1!==(u=r.indexOf(a,c+o))&&!(u<=c);){for(var f=c;f=u)n[f]=null,r=r.substr(0,f)+" "+r.substr(f+1);else if(null!==n[f]){var h=n[f].indexOf(e[0]);-1===h?n[f]+=e:l&&(n[f]=n[f].substr(0,h+1)+(1+parseInt(n[f][h+1]))+n[f].substr(h+2))}var p=c+o,d=r.substr(p,u-p).indexOf(i);c=-1!==d?d:u+s}return n}function u(t,e){var r=n(t,128);return e?a(r.cells,r.positions,.25):{edges:r.cells,positions:r.positions}}function f(t,e,r,n){var i=u(t,n),a=function(t,e,r){for(var n=e.textAlign||"start",i=e.textBaseline||"alphabetic",a=[1<<30,1<<30],o=[0,0],s=t.length,l=0;l=0?e[a]:i}))},has___:{value:y((function(e){var n=v(e);return n?r in n:t.indexOf(e)>=0}))},set___:{value:y((function(n,i){var a,o=v(n);return o?o[r]=i:(a=t.indexOf(n))>=0?e[a]=i:(a=t.length,e[a]=i,t[a]=n),this}))},delete___:{value:y((function(n){var i,a,o=v(n);return o?r in o&&delete o[r]:!((i=t.indexOf(n))<0)&&(a=t.length-1,t[i]=void 0,e[i]=e[a],t[i]=t[a],t.length=a,e.length=a,!0)}))}})};d.prototype=Object.create(Object.prototype,{get:{value:function(t,e){return this.get___(t,e)},writable:!0,configurable:!0},has:{value:function(t){return this.has___(t)},writable:!0,configurable:!0},set:{value:function(t,e){return this.set___(t,e)},writable:!0,configurable:!0},delete:{value:function(t){return this.delete___(t)},writable:!0,configurable:!0}}),"function"==typeof r?function(){function n(){this instanceof d||x();var e,n=new r,i=void 0,a=!1;return e=t?function(t,e){return n.set(t,e),n.has(t)||(i||(i=new d),i.set(t,e)),this}:function(t,e){if(a)try{n.set(t,e)}catch(r){i||(i=new d),i.set___(t,e)}else n.set(t,e);return this},Object.create(d.prototype,{get___:{value:y((function(t,e){return i?n.has(t)?n.get(t):i.get___(t,e):n.get(t,e)}))},has___:{value:y((function(t){return n.has(t)||!!i&&i.has___(t)}))},set___:{value:y(e)},delete___:{value:y((function(t){var e=!!n.delete(t);return i&&i.delete___(t)||e}))},permitHostObjects___:{value:y((function(t){if(t!==m)throw new Error("bogus call to permitHostObjects___");a=!0}))}})}t&&"undefined"!=typeof Proxy&&(Proxy=void 0),n.prototype=d.prototype,e.exports=n,Object.defineProperty(WeakMap.prototype,"constructor",{value:WeakMap,enumerable:!1,configurable:!0,writable:!0})}():("undefined"!=typeof Proxy&&(Proxy=void 0),e.exports=d)}function m(t){t.permitHostObjects___&&t.permitHostObjects___(m)}function g(t){return!("weakmap:"==t.substr(0,"weakmap:".length)&&"___"===t.substr(t.length-3))}function v(t){if(t!==Object(t))throw new TypeError("Not an object: "+t);var e=t[l];if(e&&e.key===t)return e;if(s(t)){e={key:t};try{return o(t,l,{value:e,writable:!1,enumerable:!1,configurable:!1}),e}catch(t){return}}}function y(t){return t.prototype=null,Object.freeze(t)}function x(){h||"undefined"==typeof console||(h=!0,console.warn("WeakMap should be invoked as new WeakMap(), not WeakMap(). This will be an error in the future."))}}()},{}],314:[function(t,e,r){var n=t("./hidden-store.js");e.exports=function(){var t={};return function(e){if(("object"!=typeof e||null===e)&&"function"!=typeof e)throw new Error("Weakmap-shim: Key must be object");var r=e.valueOf(t);return r&&r.identity===t?r:n(e,t)}}},{"./hidden-store.js":315}],315:[function(t,e,r){e.exports=function(t,e){var r={identity:e},n=t.valueOf;return Object.defineProperty(t,"valueOf",{value:function(t){return t!==e?n.apply(this,arguments):r},writable:!0}),r}},{}],316:[function(t,e,r){var n=t("./create-store.js");e.exports=function(){var t=n();return{get:function(e,r){var n=t(e);return n.hasOwnProperty("value")?n.value:r},set:function(e,r){return t(e).value=r,this},has:function(e){return"value"in t(e)},delete:function(e){return delete t(e).value}}}},{"./create-store.js":314}],317:[function(t,e,r){"use strict";var n,i=function(){return function(t,e,r,n,i,a){var o=t[0],s=r[0],l=[0],c=s;n|=0;var u=0,f=s;for(u=0;u=0!=p>=0&&i.push(l[0]+.5+.5*(h+p)/(h-p)),n+=f,++l[0]}}};e.exports=(n={funcName:{funcName:"zeroCrossings"}.funcName},function(t){var e={};return function(r,n,i){var a=r.dtype,o=r.order,s=[a,o.join()].join(),l=e[s];return l||(e[s]=l=t([a,o])),l(r.shape.slice(0),r.data,r.stride,0|r.offset,n,i)}}(i.bind(void 0,n)))},{}],318:[function(t,e,r){"use strict";e.exports=function(t,e){var r=[];return e=+e||0,n(t.hi(t.shape[0]-1),r,e),r};var n=t("./lib/zc-core")},{"./lib/zc-core":317}]},{},[6])(6)}))}).call(this)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}]},{},[27])(27)})); diff --git a/hardware-testing/hardware_testing/tools/plot/server.py b/hardware-testing/hardware_testing/tools/plot/server.py index b3bda88917f..72149fc959f 100644 --- a/hardware-testing/hardware_testing/tools/plot/server.py +++ b/hardware-testing/hardware_testing/tools/plot/server.py @@ -2,6 +2,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer import json from pathlib import Path +from time import time from typing import List, Any from hardware_testing.data import create_folder_for_test_data @@ -67,8 +68,9 @@ def _list_file_paths_in_directory( for sub_p in sub_dir_paths: _ret.append(sub_p) # sort newest to oldest - _ret.sort(key=lambda f: f.stem) # name includes timestamp - _ret.reverse() + # NOTE: system time on machines in SZ will randomly switch to the wrong time + # so here we can sort relative to whatever the current system time is + _ret.sort(key=lambda f: abs(time() - f.stat().st_mtime)) return _ret def _respond_to_data_request(self) -> None: diff --git a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py index 8803859d741..4044b491f0e 100644 --- a/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py +++ b/hardware/opentrons_hardware/drivers/can_bus/can_messenger.py @@ -126,7 +126,7 @@ def handle_ack(self, message: _AckResponses, arbitration_id: ArbitrationId) -> N """Add the ack to the queue if it matches the message_index of the sent message.""" if message.payload.message_index == self._message.payload.message_index: self._remove_response_node(arbitration_id.parts.originating_node_id) - self._ack_queue.put_nowait((arbitration_id, message)) + self._ack_queue.put_nowait((arbitration_id, message)) # If we've recieved all responses exit the listener if len(self._expected_nodes) == 0: self._event.set() diff --git a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py index 8525b04459a..1e31028957e 100644 --- a/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py +++ b/hardware/opentrons_hardware/firmware_bindings/messages/payloads.py @@ -319,13 +319,13 @@ def __post_init__(self) -> None: raise InternalMessageFormatError( f"FirmwareUpdateData: Data address needs to be doubleword aligned." f" {address} mod 8 equals {address % 8} and should be 0", - detail={"address": address}, + detail={"address": str(address)}, ) if data_length > FirmwareUpdateDataField.NUM_BYTES: raise InternalMessageFormatError( f"FirmwareUpdateData: Data cannot be more than" f" {FirmwareUpdateDataField.NUM_BYTES} bytes got {data_length}.", - detail={"size": data_length}, + detail={"size": str(data_length)}, ) @classmethod diff --git a/hardware/opentrons_hardware/hardware_control/move_group_runner.py b/hardware/opentrons_hardware/hardware_control/move_group_runner.py index 29b6aa89267..e906ea93646 100644 --- a/hardware/opentrons_hardware/hardware_control/move_group_runner.py +++ b/hardware/opentrons_hardware/hardware_control/move_group_runner.py @@ -617,7 +617,7 @@ async def _run_one_group(self, group_id: int, can_messenger: CanMessenger) -> No detail={ "missing-nodes": missing_node_msg, "full-timeout": str(full_timeout), - "expected-time": expected_time, + "expected-time": str(expected_time), "elapsed": str(time.time() - start_time), }, ) diff --git a/hardware/opentrons_hardware/hardware_control/tool_sensors.py b/hardware/opentrons_hardware/hardware_control/tool_sensors.py index 34f18b87542..a0f9fbcd745 100644 --- a/hardware/opentrons_hardware/hardware_control/tool_sensors.py +++ b/hardware/opentrons_hardware/hardware_control/tool_sensors.py @@ -186,7 +186,7 @@ async def capacitive_probe( detail={ "tool": tool.name, "sensor": sensor_id.name, - "threshold": relative_threshold_pf, + "threshold": str(relative_threshold_pf), }, ) LOG.info(f"starting capacitive probe with threshold {threshold.to_float()}") diff --git a/hardware/opentrons_hardware/instruments/gripper/serials.py b/hardware/opentrons_hardware/instruments/gripper/serials.py index 09f41d92dd6..6691aa64fea 100644 --- a/hardware/opentrons_hardware/instruments/gripper/serials.py +++ b/hardware/opentrons_hardware/instruments/gripper/serials.py @@ -70,6 +70,6 @@ def gripper_serial_val_from_parts(model: int, serialval: bytes) -> bytes: except struct.error as e: raise InvalidInstrumentData( message="Invalid serial data", - detail={"model": model, "serial": serialval}, + detail={"model": str(model), "serial": str(serialval)}, wrapping=[PythonException(e)], ) diff --git a/hardware/opentrons_hardware/instruments/pipettes/serials.py b/hardware/opentrons_hardware/instruments/pipettes/serials.py index b7b43cd3b59..e697821373a 100644 --- a/hardware/opentrons_hardware/instruments/pipettes/serials.py +++ b/hardware/opentrons_hardware/instruments/pipettes/serials.py @@ -93,6 +93,6 @@ def serial_val_from_parts(name: PipetteName, model: int, serialval: bytes) -> by except struct.error as e: raise InvalidInstrumentData( message="Invalid pipette serial", - detail={"name": name, "model": model, "serial": str(serialval)}, + detail={"name": str(name), "model": str(model), "serial": str(serialval)}, wrapping=[PythonException(e)], ) diff --git a/labware-designer/src/organisms/CreateLabwareSandbox/index.tsx b/labware-designer/src/organisms/CreateLabwareSandbox/index.tsx index 1c67040244d..b619f3abb6f 100644 --- a/labware-designer/src/organisms/CreateLabwareSandbox/index.tsx +++ b/labware-designer/src/organisms/CreateLabwareSandbox/index.tsx @@ -21,13 +21,16 @@ import { import { createIrregularLabware, createRegularLabware, + getPositionFromSlotId, } from '@opentrons/shared-data' -import standardDeckDef from '@opentrons/shared-data/deck/definitions/3/ot2_standard.json' +import standardDeckDef from '@opentrons/shared-data/deck/definitions/4/ot2_standard.json' import { IRREGULAR_OPTIONS, REGULAR_OPTIONS } from './fixtures' -import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { DeckDefinition, LabwareDefinition2 } from '@opentrons/shared-data' -const SLOT_OPTIONS = standardDeckDef.locations.orderedSlots.map(slot => slot.id) +const SLOT_OPTIONS = standardDeckDef.locations.addressableAreas.map( + slot => slot.id +) const DEFAULT_LABWARE_SLOT = SLOT_OPTIONS[0] const SlotSelect = styled.select` @@ -177,12 +180,20 @@ export function CreateLabwareSandbox(): JSX.Element { {viewOnDeck ? ( - - {({ deckSlotsById }) => { - const lwSlot = deckSlotsById[labwareSlot] + + {() => { + const lwPosition = getPositionFromSlotId( + labwareSlot, + (standardDeckDef as unknown) as DeckDefinition + ) return ( { - const namespace = global._fs_namespace - const fs = namespace ? global[namespace] : null - return fs || null -} - -export const shutdownFullstory = (): void => { - console.debug('shutting down Fullstory') - const fs = _getFullstory() - if (fs && fs.shutdown) { - fs.shutdown() - } - if (global._fs_namespace && global[global._fs_namespace]) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete global[global._fs_namespace] - } -} - -export const inferFsKeyWithSuffix = ( - key: string, - value: unknown -): string | null => { - // semi-hacky way to provide FS with type suffix for keys in FS `properties` - if (typeof value === 'boolean') return 'bool' - if (Number.isInteger(value)) return 'int' - if (typeof value === 'number') return 'real' - if (value instanceof Date) return '' - if (typeof value === 'string') return 'str' - - // flat array - if (Array.isArray(value) && value.every(x => !Array.isArray(x))) { - const recursiveContents = value.map(x => inferFsKeyWithSuffix(key, x)) - // homogenously-typed array - if (uniq(recursiveContents).length === 1 && recursiveContents[0] != null) { - // add 's' to suffix to denote array of type (eg 'bools') - return `${recursiveContents[0]}s` - } - } - - // NOTE: nested objects are valid in FS properties, - // but not yet supported by this fn - console.info(`could not determine Fullstory key suffix for key "${key}"`) - - return null -} - -export const fullstoryEvent = ( - name: string, - parameters: Record = {} -): void => { - // NOTE: make sure user has opted in before calling this fn - const fs = _getFullstory() - if (fs && fs.event) { - // NOTE: fullstory requires property names to have type suffix - // https://help.fullstory.com/hc/en-us/articles/360020623234#Custom%20Property%20Name%20Requirements - const _parameters = Object.keys(parameters).reduce((acc, key) => { - const value = parameters[key] - const suffix = inferFsKeyWithSuffix(key, value) - const name: string = suffix === null ? key : `${key}_${suffix}` - return { ...acc, [name]: value } - }, {}) - fs.event(name, _parameters) - } -} - -export const _setAnalyticsTags = (): void => { - const fs = _getFullstory() - // NOTE: fullstory expects the keys 'displayName' and 'email' verbatim - // though all other key names must be fit the schema described here - // https://help.fullstory.com/hc/en-us/articles/360020623294 - if (fs && fs.setUserVars) { - // eslint-disable-next-line @typescript-eslint/naming-convention - const version_str = LL_VERSION - // eslint-disable-next-line @typescript-eslint/naming-convention - const buildDate_date = LL_BUILD_DATE - - fs.setUserVars({ - ot_application_name_str: 'labware-library', // NOTE: to distinguish from other apps using the FULLSTORY_ORG - version_str, - buildDate_date, - }) - } -} diff --git a/labware-library/src/analytics/index.ts b/labware-library/src/analytics/index.ts index 58321499aa6..78a0760ea83 100644 --- a/labware-library/src/analytics/index.ts +++ b/labware-library/src/analytics/index.ts @@ -1,6 +1,5 @@ import { getAnalyticsState } from './utils' import { trackWithMixpanel } from './mixpanel' -import { fullstoryEvent } from './fullstory' import type { AnalyticsEvent } from './types' // NOTE: right now we report with only mixpanel, this fn is meant @@ -13,6 +12,5 @@ export const reportEvent = (event: AnalyticsEvent): void => { console.debug('Trackable event', { event, optedIn }) if (optedIn) { trackWithMixpanel(event.name, event.properties) - fullstoryEvent(event.name, event.properties) } } diff --git a/labware-library/src/analytics/initializeFullstory.ts b/labware-library/src/analytics/initializeFullstory.ts deleted file mode 100644 index 28991f6feab..00000000000 --- a/labware-library/src/analytics/initializeFullstory.ts +++ /dev/null @@ -1,64 +0,0 @@ -// @ts-nocheck -'use strict' -import { _setAnalyticsTags } from './fullstory' -const FULLSTORY_NAMESPACE = 'FS' -const FULLSTORY_ORG = process.env.OT_LL_FULLSTORY_ORG -export const initializeFullstory = (): void => { - console.debug('initializing Fullstory') - // NOTE: this code snippet is distributed by Fullstory, last updated 2019-10-04 - global._fs_debug = false - global._fs_host = 'fullstory.com' - global._fs_org = FULLSTORY_ORG - global._fs_namespace = FULLSTORY_NAMESPACE - ;(function (m, n, e, t, l, o, g, y) { - if (e in m) { - if (m.console && m.console.log) { - m.console.log( - 'FullStory namespace conflict. Please set window["_fs_namespace"].' - ) - } - return - } - g = m[e] = function (a, b, s) { - g.q ? g.q.push([a, b, s]) : g._api(a, b, s) - } - g.q = [] - o = n.createElement(t) - o.async = 1 - o.crossOrigin = 'anonymous' - o.src = 'https://' + global._fs_host + '/s/fs.js' - y = n.getElementsByTagName(t)[0] - y.parentNode.insertBefore(o, y) - g.identify = function (i, v, s) { - g(l, { uid: i }, s) - if (v) g(l, v, s) - } - g.setUserVars = function (v, s) { - g(l, v, s) - } - g.event = function (i, v, s) { - g('event', { n: i, p: v }, s) - } - g.shutdown = function () { - g('rec', !1) - } - g.restart = function () { - g('rec', !0) - } - g.log = function (a, b) { - g('log', [a, b]) - } - g.consent = function (a) { - g('consent', !arguments.length || a) - } - g.identifyAccount = function (i, v) { - o = 'account' - v = v || {} - v.acctId = i - g(o, v) - } - g.clearUserCookie = function () {} - })(global, global.document, global._fs_namespace, 'script', 'user') - - _setAnalyticsTags() -} diff --git a/labware-library/src/analytics/utils.ts b/labware-library/src/analytics/utils.ts index bf6ba2b5ee9..6b7e11e31aa 100644 --- a/labware-library/src/analytics/utils.ts +++ b/labware-library/src/analytics/utils.ts @@ -1,8 +1,6 @@ import cookie from 'cookie' import { initializeMixpanel, mixpanelOptIn, mixpanelOptOut } from './mixpanel' -import { initializeFullstory } from './initializeFullstory' -import { shutdownFullstory } from './fullstory' import type { AnalyticsState } from './types' const COOKIE_KEY_NAME = 'ot_ll_analytics' // NOTE: cookie is named "LL" but only LC uses it now @@ -62,16 +60,12 @@ export const getAnalyticsState = (): AnalyticsState => { return state } -// NOTE: Fullstory has no opt-in/out, control by adding/removing it completely - export const persistAnalyticsState = (state: AnalyticsState): void => { persistAnalyticsCookie(state) if (state.optedIn) { mixpanelOptIn() - initializeFullstory() } else { mixpanelOptOut() - shutdownFullstory() } } diff --git a/labware-library/src/components/LabwareList/__tests__/__snapshots__/LabwareList.test.tsx.snap b/labware-library/src/components/LabwareList/__tests__/__snapshots__/LabwareList.test.tsx.snap index 4fc515c877c..aeb4b5c916d 100644 --- a/labware-library/src/components/LabwareList/__tests__/__snapshots__/LabwareList.test.tsx.snap +++ b/labware-library/src/components/LabwareList/__tests__/__snapshots__/LabwareList.test.tsx.snap @@ -6258,6 +6258,1193 @@ exports[`LabwareList component renders 1`] = ` } key="fixture/fixture_96_plate/1" /> + + +
  • + + Adapter + +
  • `; diff --git a/labware-library/typings/fullstory.d.ts b/labware-library/typings/fullstory.d.ts deleted file mode 100644 index 1fbc7c9b6ed..00000000000 --- a/labware-library/typings/fullstory.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -// TODO(mc, 2021-03-31): migrate to @fullstory/browser npm package -// adapted from https://github.com/fullstorydev/fullstory-browser-sdk/blob/master/src/index.d.ts - -declare namespace FullStory { - interface SnippetOptions { - orgId: string - namespace?: string - debug?: boolean - host?: string - script?: string - recordCrossDomainIFrames?: boolean - recordOnlyThisIFrame?: boolean // see README for details - devMode?: boolean - } - - interface UserVars { - displayName?: string - email?: string - [key: string]: any - } - - type LogLevel = 'log' | 'info' | 'warn' | 'error' | 'debug' - - interface FullStory { - anonymize: () => void - consent: (userConsents?: boolean) => void - event: (eventName: string, eventProperties: { [key: string]: any }) => void - identify: (uid: string, customVars?: UserVars) => void - init: (options: SnippetOptions) => void - log: ((level: LogLevel, msg: string) => void) & ((msg: string) => void) - restart: () => void - setUserVars: (customVars: UserVars) => void - shutdown: () => void - } -} - -declare module NodeJS { - interface Global { - _fs_namespace: 'FS' | undefined - FS: FullStory.FullStory | undefined - } -} diff --git a/protocol-designer/README.md b/protocol-designer/README.md index efc3c811b3b..c7bf1b3983a 100644 --- a/protocol-designer/README.md +++ b/protocol-designer/README.md @@ -53,10 +53,6 @@ Used for analytics segmentation. Also saved in protocol file at `designer-applic Used for analytics segmentation. In Travis CI, this is fed by `$TRAVIS_COMMIT`. -### `OT_PD_FULLSTORY_ORG` - -Used for FullStory. Should be provided in the Travis build. - ### `OT_PD_MIXPANEL_ID` Used for Mixpanel in prod. Should be provided in the CI build. diff --git a/protocol-designer/cypress/integration/migrations.spec.js b/protocol-designer/cypress/integration/migrations.spec.js index ec21b08994e..400e63e5268 100644 --- a/protocol-designer/cypress/integration/migrations.spec.js +++ b/protocol-designer/cypress/integration/migrations.spec.js @@ -130,7 +130,7 @@ describe('Protocol fixtures migrate and match snapshots', () => { cy.get('div') .contains( - 'This protocol can only run on app and robot server version 7.0 or higher' + 'This protocol can only run on app and robot server version 7.1 or higher' ) .should('exist') cy.get('button').contains('continue', { matchCase: false }).click() diff --git a/protocol-designer/src/analytics/actions.ts b/protocol-designer/src/analytics/actions.ts index 05a6466be06..eee86cd900a 100644 --- a/protocol-designer/src/analytics/actions.ts +++ b/protocol-designer/src/analytics/actions.ts @@ -1,4 +1,3 @@ -import { initializeFullstory, shutdownFullstory } from './fullstory' import { setMixpanelTracking, AnalyticsEvent } from './mixpanel' export interface SetOptIn { @@ -9,10 +8,8 @@ export interface SetOptIn { const _setOptIn = (payload: SetOptIn['payload']): SetOptIn => { // side effects if (payload) { - initializeFullstory() setMixpanelTracking(true) } else { - shutdownFullstory() setMixpanelTracking(false) } diff --git a/protocol-designer/src/analytics/fullstory.ts b/protocol-designer/src/analytics/fullstory.ts deleted file mode 100644 index 023bccec7a1..00000000000 --- a/protocol-designer/src/analytics/fullstory.ts +++ /dev/null @@ -1,91 +0,0 @@ -// @ts-nocheck -import cookie from 'cookie' - -export const shutdownFullstory = (): void => { - if (window[window._fs_namespace]) { - window[window._fs_namespace].shutdown() - } - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete window[window._fs_namespace] -} - -const _setAnalyticsTags = (): void => { - const cookies = cookie.parse(global.document.cookie) - const { ot_email: email, ot_name: displayName } = cookies - const commit_str = process.env.OT_PD_COMMIT_HASH - const version_str = process.env.OT_PD_VERSION - const buildDate_date = new Date(process.env.OT_PD_BUILD_DATE as any) - - // NOTE: fullstory expects the keys 'displayName' and 'email' verbatim - // though all other key names must be fit the schema described here - // https://help.fullstory.com/develop-js/137380 - if (window[window._fs_namespace]) { - window[window._fs_namespace].setUserVars({ - displayName, - email, - commit_str, - version_str, - buildDate_date, - ot_application_name_str: 'protocol-designer', // NOTE: to distinguish from other apps using the org - }) - } -} - -// NOTE: this code snippet is distributed by Fullstory and formatting has been maintained -window._fs_debug = false -window._fs_host = 'fullstory.com' -window._fs_org = process.env.OT_PD_FULLSTORY_ORG -window._fs_namespace = 'FS' - -export const initializeFullstory = (): void => { - ;(function (m, n, e, t, l, o, g: any, y: any) { - if (e in m) { - if (m.console && m.console.log) { - m.console.log( - 'Fullstory namespace conflict. Please set window["_fs_namespace"].' - ) - } - return - } - g = m[e] = function (a, b, s) { - g.q ? g.q.push([a, b, s]) : g._api(a, b, s) - } - g.q = [] - o = n.createElement(t) - o.async = 1 - o.crossOrigin = 'anonymous' - o.src = 'https://' + global._fs_host + '/s/fs.js' - y = n.getElementsByTagName(t)[0] - y.parentNode.insertBefore(o, y) - g.identify = function (i, v, s) { - g(l, { uid: i }, s) - if (v) g(l, v, s) - } - g.setUserVars = function (v, s) { - g(l, v, s) - } - g.event = function (i, v, s) { - g('event', { n: i, p: v }, s) - } - g.shutdown = function () { - g('rec', !1) - } - g.restart = function () { - g('rec', !0) - } - g.log = function (a, b) { - g('log', [a, b]) - } - g.consent = function (a) { - g('consent', !arguments.length || a) - } - g.identifyAccount = function (i, v) { - o = 'account' - v = v || {} - v.acctId = i - g(o, v) - } - g.clearUserCookie = function () {} - })(global, global.document, global._fs_namespace, 'script', 'user') - _setAnalyticsTags() -} diff --git a/protocol-designer/src/components/DeckSetup/DeckSetup.css b/protocol-designer/src/components/DeckSetup/DeckSetup.css index f6641390146..ac65e2975a5 100644 --- a/protocol-designer/src/components/DeckSetup/DeckSetup.css +++ b/protocol-designer/src/components/DeckSetup/DeckSetup.css @@ -2,6 +2,7 @@ .deck_wrapper { flex: 1; + margin-top: 1rem; } .deck_header { diff --git a/protocol-designer/src/components/DeckSetup/FlexModuleTag.tsx b/protocol-designer/src/components/DeckSetup/FlexModuleTag.tsx index 6e9d28cead7..9a8ce2920b5 100644 --- a/protocol-designer/src/components/DeckSetup/FlexModuleTag.tsx +++ b/protocol-designer/src/components/DeckSetup/FlexModuleTag.tsx @@ -18,13 +18,14 @@ export function FlexModuleTag(props: FlexModuleTagProps): JSX.Element { return ( diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx index 4605263e826..430438c896e 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx @@ -23,7 +23,7 @@ import { import { START_TERMINAL_ITEM_ID, TerminalItemId } from '../../../steplist' import { BlockedSlot } from './BlockedSlot' -import type { DeckSlot as DeckSlotDefinition } from '@opentrons/shared-data' +import type { CoordinateTuple, Dimensions } from '@opentrons/shared-data' import type { BaseState, DeckSlot, ThunkDispatch } from '../../../types' import type { LabwareOnDeck } from '../../../step-forms' @@ -37,7 +37,8 @@ interface DNDP { } interface OP { - slot: DeckSlotDefinition & { id: DeckSlot } + slotPosition: CoordinateTuple + slotBoundingBox: Dimensions // labwareId is the adapter's labwareId labwareId: string allLabware: LabwareOnDeck[] @@ -61,7 +62,8 @@ export const AdapterControlsComponents = ( props: SlotControlsProps ): JSX.Element | null => { const { - slot, + slotPosition, + slotBoundingBox, addLabware, selectedTerminalItemId, isOver, @@ -104,18 +106,18 @@ export const AdapterControlsComponents = ( {slotBlocked ? ( ) : ( unknown setDraggedLabware: (labware?: LabwareOnDeck | null) => unknown swapBlocked: boolean @@ -24,7 +25,7 @@ interface LabwareControlsProps { export const LabwareControls = (props: LabwareControlsProps): JSX.Element => { const { labwareOnDeck, - slot, + slotPosition, selectedTerminalItemId, setHoveredLabware, setDraggedLabware, @@ -32,7 +33,7 @@ export const LabwareControls = (props: LabwareControlsProps): JSX.Element => { } = props const canEdit = selectedTerminalItemId === START_TERMINAL_ITEM_ID - const [x, y] = slot.position + const [x, y] = slotPosition const width = labwareOnDeck.def.dimensions.xDimension const height = labwareOnDeck.def.dimensions.yDimension return ( diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx index ca2fb3a9fdd..b487dcd1b53 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx @@ -23,7 +23,8 @@ import { START_TERMINAL_ITEM_ID, TerminalItemId } from '../../../steplist' import { BlockedSlot } from './BlockedSlot' import type { - DeckSlot as DeckSlotDefinition, + CoordinateTuple, + Dimensions, ModuleType, } from '@opentrons/shared-data' import type { BaseState, DeckSlot, ThunkDispatch } from '../../../types' @@ -38,7 +39,10 @@ interface DNDP { } interface OP { - slot: DeckSlotDefinition & { id: DeckSlot } // NOTE: Ian 2019-10-22 make slot `id` more restrictive when used in PD + slotPosition: CoordinateTuple | null + slotBoundingBox: Dimensions + // NOTE: slotId can be either AddressableAreaName or moduleId + slotId: string moduleType: ModuleType | null selectedTerminalItemId?: TerminalItemId | null handleDragHover?: () => unknown @@ -59,7 +63,8 @@ export const SlotControlsComponent = ( props: SlotControlsProps ): JSX.Element | null => { const { - slot, + slotBoundingBox, + slotPosition, addLabware, selectedTerminalItemId, isOver, @@ -71,7 +76,8 @@ export const SlotControlsComponent = ( } = props if ( selectedTerminalItemId !== START_TERMINAL_ITEM_ID || - (itemType !== DND_TYPES.LABWARE && itemType !== null) + (itemType !== DND_TYPES.LABWARE && itemType !== null) || + slotPosition == null ) return null @@ -107,18 +113,18 @@ export const SlotControlsComponent = ( {slotBlocked ? ( ) : ( , ownProps: OP ): DP => ({ - addLabware: () => dispatch(openAddLabwareModal({ slot: ownProps.slot.id })), + addLabware: () => dispatch(openAddLabwareModal({ slot: ownProps.slotId })), moveDeckItem: (sourceSlot, destSlot) => dispatch(moveDeckItem(sourceSlot, destSlot)), }) @@ -155,7 +161,7 @@ const slotTarget = { drop: (props: SlotControlsProps, monitor: DropTargetMonitor) => { const draggedItem = monitor.getItem() if (draggedItem) { - props.moveDeckItem(draggedItem.labwareOnDeck.slot, props.slot.id) + props.moveDeckItem(draggedItem.labwareOnDeck.slot, props.slotId) } }, hover: (props: SlotControlsProps) => { diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx index b6ae460ab10..12ba722b0e3 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx @@ -2,7 +2,7 @@ import React from 'react' import { shallow } from 'enzyme' import fixture_96_plate from '@opentrons/shared-data/labware/fixtures/2/fixture_96_plate.json' import { - DeckSlot, + CoordinateTuple, LabwareDefinition2, MAGNETIC_MODULE_TYPE, } from '@opentrons/shared-data' @@ -18,16 +18,12 @@ describe('SlotControlsComponent', () => { typeof labwareModuleCompatibility.getLabwareIsCompatible > beforeEach(() => { - const slot: DeckSlot = { - id: 'deckSlot1', - position: [1, 2, 3], - boundingBox: { - xDimension: 10, - yDimension: 20, - zDimension: 40, - }, - displayName: 'slot 1', - compatibleModules: [MAGNETIC_MODULE_TYPE], + const slotId = 'D1' + const slotPosition: CoordinateTuple = [1, 2, 3] + const slotBoundingBox = { + xDimension: 10, + yDimension: 20, + zDimension: 40, } const labwareOnDeck = { @@ -38,7 +34,9 @@ describe('SlotControlsComponent', () => { } props = { - slot, + slotId, + slotPosition, + slotBoundingBox, addLabware: jest.fn(), moveDeckItem: jest.fn(), selectedTerminalItemId: START_TERMINAL_ITEM_ID, diff --git a/protocol-designer/src/components/DeckSetup/SlotLabels.tsx b/protocol-designer/src/components/DeckSetup/SlotLabels.tsx index a357197e98c..0efcc8a5a3b 100644 --- a/protocol-designer/src/components/DeckSetup/SlotLabels.tsx +++ b/protocol-designer/src/components/DeckSetup/SlotLabels.tsx @@ -15,6 +15,7 @@ import type { RobotType } from '@opentrons/shared-data' interface SlotLabelsProps { robotType: RobotType hasStagingAreas: boolean + hasWasteChute: boolean } /** @@ -25,6 +26,7 @@ interface SlotLabelsProps { export const SlotLabels = ({ robotType, hasStagingAreas, + hasWasteChute, }: SlotLabelsProps): JSX.Element | null => { return robotType === FLEX_ROBOT_TYPE ? ( <> @@ -59,7 +61,7 @@ export const SlotLabels = ({ height="2.5rem" width={hasStagingAreas ? '40.5rem' : '30.375rem'} x="-15" - y="-65" + y={hasWasteChute ? '-90' : '-65'} > { .calledWith( partialComponentPropsMatcher({ width: 5, - height: 16, + height: 20, }) ) .mockImplementation(({ children }) => ( @@ -48,7 +48,7 @@ describe('FlexModuleTag', () => { .calledWith( partialComponentPropsMatcher({ width: 5, - height: 16, + height: 20, }) ) .mockImplementation(({ children }) => ( diff --git a/protocol-designer/src/components/DeckSetup/constants.ts b/protocol-designer/src/components/DeckSetup/constants.ts index 8d2a436291c..2037126b253 100644 --- a/protocol-designer/src/components/DeckSetup/constants.ts +++ b/protocol-designer/src/components/DeckSetup/constants.ts @@ -1,26 +1,3 @@ -import { STANDARD_SLOT_LOAD_NAME } from '@opentrons/shared-data' - -const cutouts = [ - 'A1', - 'A2', - 'A3', - 'B1', - 'B2', - 'B3', - 'C1', - 'C2', - 'C3', - 'D1', - 'D2', - 'D3', -] - -export const DEFAULT_SLOTS = cutouts.map((cutout, index) => ({ - fixtureId: (index + 1).toString(), - fixtureLocation: cutout, - loadName: STANDARD_SLOT_LOAD_NAME, -})) - export const VIEWBOX_MIN_X = -64 export const VIEWBOX_MIN_Y = -10 export const VIEWBOX_WIDTH = 520 diff --git a/protocol-designer/src/components/DeckSetup/index.tsx b/protocol-designer/src/components/DeckSetup/index.tsx index 8197275779a..74d93f4b16e 100644 --- a/protocol-designer/src/components/DeckSetup/index.tsx +++ b/protocol-designer/src/components/DeckSetup/index.tsx @@ -3,19 +3,18 @@ import { useDispatch, useSelector } from 'react-redux' import compact from 'lodash/compact' import values from 'lodash/values' import { - useOnClickOutside, - RobotWorkSpaceRenderProps, - Module, COLORS, - TrashLocation, + DeckFromLayers, FlexTrash, + Module, RobotCoordinateSpaceWithDOMCoords, - WasteChuteFixture, - WasteChuteLocation, + RobotWorkSpaceRenderProps, + SingleSlotFixture, StagingAreaFixture, StagingAreaLocation, - SingleSlotFixture, - DeckFromData, + TrashCutoutId, + useOnClickOutside, + WasteChuteFixture, } from '@opentrons/components' import { AdditionalEquipmentEntity, @@ -23,31 +22,27 @@ import { ModuleTemporalProperties, } from '@opentrons/step-generation' import { - getLabwareHasQuirk, - inferModuleOrientationFromSlot, - DeckSlot as DeckDefSlot, + FLEX_ROBOT_TYPE, + getAddressableAreaFromSlotId, getDeckDefFromRobotType, - OT2_ROBOT_TYPE, + getLabwareHasQuirk, getModuleDef2, + getModuleDisplayName, + getPositionFromSlotId, + inferModuleOrientationFromSlot, inferModuleOrientationFromXCoordinate, + isAddressableAreaStandardSlot, + OT2_ROBOT_TYPE, + STAGING_AREA_CUTOUTS, THERMOCYCLER_MODULE_TYPE, - getModuleDisplayName, - DeckDefinition, - RobotType, - FLEX_ROBOT_TYPE, - Cutout, - TRASH_BIN_LOAD_NAME, - STAGING_AREA_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' -import { - FLEX_TRASH_DEF_URI, - OT_2_TRASH_DEF_URI, - PSEUDO_DECK_SLOTS, -} from '../../constants' +import { FLEX_TRASH_DEF_URI, OT_2_TRASH_DEF_URI } from '../../constants' import { selectors as labwareDefSelectors } from '../../labware-defs' import { selectors as featureFlagSelectors } from '../../feature-flags' +import { getStagingAreaAddressableAreas } from '../../utils' import { getSlotIdsBlockedBySpanning, getSlotIsEmpty, @@ -73,9 +68,16 @@ import { import { FlexModuleTag } from './FlexModuleTag' import { Ot2ModuleTag } from './Ot2ModuleTag' import { SlotLabels } from './SlotLabels' -import { DEFAULT_SLOTS } from './constants' import { getHasGen1MultiChannelPipette, getSwapBlocked } from './utils' +import type { + AddressableAreaName, + CutoutFixture, + CutoutId, + DeckDefinition, + RobotType, +} from '@opentrons/shared-data' + import styles from './DeckSetup.css' export const DECK_LAYER_BLOCKLIST = [ @@ -88,12 +90,24 @@ export const DECK_LAYER_BLOCKLIST = [ 'screwHoles', ] -type ContentsProps = RobotWorkSpaceRenderProps & { +const OT2_STANDARD_DECK_VIEW_LAYER_BLOCK_LIST: string[] = [ + 'calibrationMarkings', + 'fixedBase', + 'doorStops', + 'metalFrame', + 'removalHandle', + 'removableDeckOutline', + 'screwHoles', +] + +interface ContentsProps { + getRobotCoordsFromDOMCoords: RobotWorkSpaceRenderProps['getRobotCoordsFromDOMCoords'] activeDeckSetup: InitialDeckSetup selectedTerminalItemId?: TerminalItemId | null showGen1MultichannelCollisionWarnings: boolean deckDef: DeckDefinition robotType: RobotType + stagingAreaCutoutIds: CutoutId[] trashSlot: string | null } @@ -103,12 +117,12 @@ const darkFill = COLORS.darkGreyEnabled export const DeckSetupContents = (props: ContentsProps): JSX.Element => { const { activeDeckSetup, - deckSlotsById, getRobotCoordsFromDOMCoords, showGen1MultichannelCollisionWarnings, deckDef, robotType, trashSlot, + stagingAreaCutoutIds, } = props // NOTE: handling module<>labware compat when moving labware to empty module // is handled by SlotControls. @@ -140,9 +154,6 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { ) const slotIdsBlockedBySpanning = getSlotIdsBlockedBySpanning(activeDeckSetup) - const deckSlots: DeckDefSlot[] = values(deckSlotsById) - // modules can be on the deck, including pseudo-slots (eg special 'spanning' slot for thermocycler position) - const moduleParentSlots = [...deckSlots, ...values(PSEUDO_DECK_SLOTS)] const allLabware: LabwareOnDeckType[] = Object.keys( activeDeckSetup.labware @@ -156,24 +167,22 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { const allModules: ModuleOnDeck[] = values(activeDeckSetup.modules) // NOTE: naively hard-coded to show warning north of slots 1 or 3 when occupied by any module - const multichannelWarningSlots: DeckDefSlot[] = showGen1MultichannelCollisionWarnings + const multichannelWarningSlotIds: AddressableAreaName[] = showGen1MultichannelCollisionWarnings ? compact([ - (allModules.some( + allModules.some( moduleOnDeck => moduleOnDeck.slot === '1' && - // @ts-expect-error(sa, 2021-6-21): ModuleModel is a super type of the elements in MODULES_WITH_COLLISION_ISSUES MODULES_WITH_COLLISION_ISSUES.includes(moduleOnDeck.model) - ) && - deckSlotsById?.['4']) || - null, - (allModules.some( + ) + ? deckDef.locations.addressableAreas.find(s => s.id === '4')?.id + : null, + allModules.some( moduleOnDeck => moduleOnDeck.slot === '3' && - // @ts-expect-error(sa, 2021-6-21): ModuleModel is a super type of the elements in MODULES_WITH_COLLISION_ISSUES MODULES_WITH_COLLISION_ISSUES.includes(moduleOnDeck.model) - ) && - deckSlotsById?.['6']) || - null, + ) + ? deckDef.locations.addressableAreas.find(s => s.id === '6')?.id + : null, ]) : [] @@ -181,10 +190,13 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { <> {/* all modules */} {allModules.map(moduleOnDeck => { - const slot = moduleParentSlots.find( - slot => slot.id === moduleOnDeck.slot - ) - if (slot == null) { + // modules can be on the deck, including pseudo-slots (eg special 'spanning' slot for thermocycler position) + // const moduleParentSlots = [...deckSlots, ...values(PSEUDO_DECK_SLOTS)] + // const slot = moduleParentSlots.find( + // slot => slot.id === moduleOnDeck.slot + // ) + const slotPosition = getPositionFromSlotId(moduleOnDeck.slot, deckDef) + if (slotPosition == null) { console.warn( `no slot ${moduleOnDeck.slot} for module ${moduleOnDeck.id}` ) @@ -225,17 +237,10 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { const shouldHideChildren = moduleOnDeck.moduleState.type === THERMOCYCLER_MODULE_TYPE && moduleOnDeck.moduleState.lidOpen === false - const labwareInterfaceSlotDef: DeckDefSlot = { - displayName: `Labware interface on ${moduleOnDeck.model}`, - id: moduleOnDeck.id, - position: [0, 0, 0], // Module Component already handles nested positioning - matingSurfaceUnitVector: [-1, 1, -1], - boundingBox: { - xDimension: moduleDef.dimensions.labwareInterfaceXDimension ?? 0, - yDimension: moduleDef.dimensions.labwareInterfaceYDimension ?? 0, - zDimension: 0, - }, - compatibleModules: [THERMOCYCLER_MODULE_TYPE], + const labwareInterfaceBoundingBox = { + xDimension: moduleDef.dimensions.labwareInterfaceXDimension ?? 0, + yDimension: moduleDef.dimensions.labwareInterfaceYDimension ?? 0, + zDimension: 0, } const moduleOrientation = inferModuleOrientationFromSlot( @@ -246,15 +251,13 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { labwareLoadedOnModule?.def.metadata.displayCategory === 'adapter' return ( {labwareLoadedOnModule != null && !shouldHideChildren ? ( @@ -265,19 +268,20 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { labwareOnDeck={labwareLoadedOnModule} /> {isAdapter ? ( - // @ts-expect-error + // @ts-expect-error ) : ( { {labwareLoadedOnModule == null && !shouldHideChildren && !isAdapter ? ( - // @ts-expect-error (ce, 2021-06-21) once we upgrade to the react-dnd hooks api, and use react-redux hooks, typing this will be easier + // @ts-expect-error ) : null} {robotType === FLEX_ROBOT_TYPE ? ( @@ -321,35 +327,52 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { })} {/* on-deck warnings */} - {multichannelWarningSlots.map(slot => ( - - ))} + {multichannelWarningSlotIds.map(slotId => { + const slotPosition = getPositionFromSlotId(slotId, deckDef) + const slotBoundingBox = getAddressableAreaFromSlotId(slotId, deckDef) + ?.boundingBox + return slotPosition != null && slotBoundingBox != null ? ( + + ) : null + })} - {/* SlotControls for all empty deck + module slots */} - {deckSlots - .filter( - slot => - !slotIdsBlockedBySpanning.includes(slot.id) && - getSlotIsEmpty(activeDeckSetup, slot.id) && - slot.id !== trashSlot - ) - .map(slot => { + {/* SlotControls for all empty deck */} + {deckDef.locations.addressableAreas + .filter(addressableArea => { + const stagingAreaAddressableAreas = getStagingAreaAddressableAreas( + stagingAreaCutoutIds + ) + const addressableAreas = + isAddressableAreaStandardSlot(addressableArea.id, deckDef) || + stagingAreaAddressableAreas.includes(addressableArea.id) + return ( + addressableAreas && + !slotIdsBlockedBySpanning.includes(addressableArea.id) && + getSlotIsEmpty(activeDeckSetup, addressableArea.id) && + addressableArea.id !== trashSlot + ) + }) + .map(addressableArea => { return ( - // @ts-expect-error (ce, 2021-06-21) once we upgrade to the react-dnd hooks api, and use react-redux hooks, typing this will be easier + // @ts-expect-error ) @@ -363,8 +386,13 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { allLabware.some(lab => lab.id === labware.slot) ) return null - const slot = deckSlots.find(slot => slot.id === labware.slot) - if (slot == null) { + + const slotPosition = getPositionFromSlotId(labware.slot, deckDef) + const slotBoundingBox = getAddressableAreaFromSlotId( + labware.slot, + deckDef + )?.boundingBox + if (slotPosition == null || slotBoundingBox == null) { console.warn(`no slot ${labware.slot} for labware ${labware.id}!`) return null } @@ -373,27 +401,26 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { return ( {labwareIsAdapter ? ( - <> - {/* @ts-expect-error */} - - + // @ts-expect-error + ) : ( { labware.slot === 'offDeck' ) return null - const slotOnDeck = deckSlots.find(slot => slot.id === labware.slot) - if (slotOnDeck != null) { + if ( + deckDef.locations.addressableAreas.some( + addressableArea => addressableArea.id === labware.slot + ) + ) { return null } const slotForOnTheDeck = allLabware.find(lab => lab.id === labware.slot) ?.slot const slotForOnMod = allModules.find(mod => mod.id === slotForOnTheDeck) ?.slot - const deckDefSlot = deckSlots.find( - s => s.id === (slotForOnMod ?? slotForOnTheDeck) - ) - if (deckDefSlot == null) { + let slotPosition = null + if (slotForOnMod != null) { + slotPosition = getPositionFromSlotId(slotForOnMod, deckDef) + } else if (slotForOnTheDeck != null) { + slotPosition = getPositionFromSlotId(slotForOnTheDeck, deckDef) + } + if (slotPosition == null) { console.warn(`no slot ${labware.slot} for labware ${labware.id}!`) return null } return ( { const trashBinFixtures = [ { - fixtureId: trash?.id, - fixtureLocation: trash?.slot as Cutout, - loadName: TRASH_BIN_LOAD_NAME, + cutoutId: + trash?.slot != null + ? getCutoutIdForAddressableArea( + trash?.slot as AddressableAreaName, + deckDef.cutoutFixtures + ) + : null, + cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE, }, ] const wasteChuteFixtures = Object.values( activeDeckSetup.additionalEquipmentOnDeck - ).filter(aE => aE.name === WASTE_CHUTE_LOAD_NAME) + ).filter(aE => WASTE_CHUTE_CUTOUT.includes(aE.location as CutoutId)) const stagingAreaFixtures: AdditionalEquipmentEntity[] = Object.values( activeDeckSetup.additionalEquipmentOnDeck - ).filter(aE => aE.name === STAGING_AREA_LOAD_NAME) - const locations = Object.values( - activeDeckSetup.additionalEquipmentOnDeck - ).map(aE => aE.location) + ).filter(aE => STAGING_AREA_CUTOUTS.includes(aE.location as CutoutId)) - const filteredSlots = DEFAULT_SLOTS.filter( - slot => !locations.includes(slot.fixtureLocation) - ) + const hasWasteChute = wasteChuteFixtures.length > 0 + const filteredAddressableAreas = deckDef.locations.addressableAreas.filter( + aa => isAddressableAreaStandardSlot(aa.id, deckDef) + ) return (
    {drilledDown && }
    - {({ deckSlotsById, getRobotCoordsFromDOMCoords }) => ( + {({ getRobotCoordsFromDOMCoords }) => ( <> {robotType === OT2_ROBOT_TYPE ? ( - + ) : ( <> - {filteredSlots.map(fixture => ( - - ))} + {filteredAddressableAreas.map(addressableArea => { + const cutoutId = getCutoutIdForAddressableArea( + addressableArea.id, + deckDef.cutoutFixtures + ) + return cutoutId != null ? ( + + ) : null + })} {stagingAreaFixtures.map(fixture => ( ))} {trash != null - ? trashBinFixtures.map(fixture => ( - - - - - )) + ? trashBinFixtures.map(({ cutoutId }) => + cutoutId != null ? ( + + + + + ) : null + ) : null} {wasteChuteFixtures.map(fixture => ( { robotType={robotType} activeDeckSetup={activeDeckSetup} selectedTerminalItemId={selectedTerminalItemId} + stagingAreaCutoutIds={stagingAreaFixtures.map( + // TODO(jr, 11/13/23): fix this type since AdditionalEquipment['location'] is type string + // instead of CutoutId + areas => areas.location as CutoutId + )} {...{ deckDef, - deckSlotsById, getRobotCoordsFromDOMCoords, showGen1MultichannelCollisionWarnings, }} @@ -594,6 +646,7 @@ export const DeckSetup = (): JSX.Element => { 0} + hasWasteChute={hasWasteChute} /> )} @@ -602,3 +655,18 @@ export const DeckSetup = (): JSX.Element => {
    ) } + +function getCutoutIdForAddressableArea( + addressableArea: AddressableAreaName, + cutoutFixtures: CutoutFixture[] +): CutoutId | null { + return cutoutFixtures.reduce((acc, cutoutFixture) => { + const [cutoutId] = + Object.entries( + cutoutFixture.providesAddressableAreas + ).find(([_cutoutId, providedAAs]) => + providedAAs.includes(addressableArea) + ) ?? [] + return (cutoutId as CutoutId) ?? acc + }, null) +} diff --git a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx index 7d247725a80..a5f563888c7 100644 --- a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx +++ b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx @@ -223,12 +223,12 @@ function getWarningContent({ return null } -export const v7WarningContent: JSX.Element = ( +export const v8WarningContent: JSX.Element = (

    - {i18n.t(`alert.hint.export_v7_protocol_7_0.body1`)}{' '} - {i18n.t(`alert.hint.export_v7_protocol_7_0.body2`)} - {i18n.t(`alert.hint.export_v7_protocol_7_0.body3`)} + {i18n.t(`alert.hint.export_v8_protocol_7_1.body1`)}{' '} + {i18n.t(`alert.hint.export_v8_protocol_7_1.body2`)} + {i18n.t(`alert.hint.export_v8_protocol_7_1.body3`)}

    ) @@ -256,13 +256,12 @@ export function FileSidebar(props: Props): JSX.Element { ) const { trashBinUnused, wasteChuteUnused } = getUnusedTrash( labwareOnDeck, - // additionalEquipment, + additionalEquipment, fileData?.commands ) const fixtureWithoutStep: Fixture = { trashBin: trashBinUnused, - // TODO(jr, 10/30/23): wire this up later when we know waste chute commands wasteChute: wasteChuteUnused, stagingAreaSlots: getUnusedStagingAreas( additionalEquipment, @@ -325,8 +324,8 @@ export function FileSidebar(props: Props): JSX.Element { content: React.ReactNode } => { return { - hintKey: 'export_v7_protocol_7_0', - content: v7WarningContent, + hintKey: 'export_v8_protocol_7_1', + content: v8WarningContent, } } diff --git a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx index 8b15ec934f1..2a19aab1d42 100644 --- a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx +++ b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx @@ -16,7 +16,7 @@ import { } from '@opentrons/shared-data/pipette/fixtures/name' import fixture_tiprack_10_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_10_ul.json' import { useBlockingHint } from '../../Hints/useBlockingHint' -import { FileSidebar, v7WarningContent } from '../FileSidebar' +import { FileSidebar, v8WarningContent } from '../FileSidebar' import { FLEX_TRASH_DEF_URI } from '@opentrons/step-generation' jest.mock('../../Hints/useBlockingHint') @@ -165,7 +165,11 @@ describe('FileSidebar', () => { // @ts-expect-error(sa, 2021-6-22): props.fileData might be null props.fileData.commands = commands props.additionalEquipment = { - [stagingArea]: { name: 'stagingArea', id: stagingArea, location: 'A3' }, + [stagingArea]: { + name: 'stagingArea', + id: stagingArea, + location: 'cutoutA3', + }, } const wrapper = shallow() @@ -273,8 +277,8 @@ describe('FileSidebar', () => { // Before save button is clicked, enabled should be false expect(mockUseBlockingHint).toHaveBeenNthCalledWith(1, { enabled: false, - hintKey: 'export_v7_protocol_7_0', - content: v7WarningContent, + hintKey: 'export_v8_protocol_7_1', + content: v8WarningContent, handleCancel: expect.any(Function), handleContinue: expect.any(Function), }) @@ -285,8 +289,8 @@ describe('FileSidebar', () => { // After save button is clicked, enabled should be true expect(mockUseBlockingHint).toHaveBeenLastCalledWith({ enabled: true, - hintKey: 'export_v7_protocol_7_0', - content: v7WarningContent, + hintKey: 'export_v8_protocol_7_1', + content: v8WarningContent, handleCancel: expect.any(Function), handleContinue: expect.any(Function), }) diff --git a/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.ts b/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.test.ts similarity index 73% rename from protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.ts rename to protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.test.ts index 333a6aee456..e95f2c06d3f 100644 --- a/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.ts +++ b/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedStagingAreas.test.ts @@ -6,7 +6,11 @@ describe('getUnusedStagingAreas', () => { it('returns true for unused staging area', () => { const stagingArea = 'stagingAreaId' const mockAdditionalEquipment = { - [stagingArea]: { name: 'stagingArea', id: stagingArea, location: 'A3' }, + [stagingArea]: { + name: 'stagingArea', + id: stagingArea, + location: 'cutoutA3', + }, } as AdditionalEquipment expect(getUnusedStagingAreas(mockAdditionalEquipment, [])).toEqual(['A4']) @@ -15,8 +19,16 @@ describe('getUnusedStagingAreas', () => { const stagingArea = 'stagingAreaId' const stagingArea2 = 'stagingAreaId2' const mockAdditionalEquipment = { - [stagingArea]: { name: 'stagingArea', id: stagingArea, location: 'A3' }, - [stagingArea2]: { name: 'stagingArea', id: stagingArea2, location: 'B3' }, + [stagingArea]: { + name: 'stagingArea', + id: stagingArea, + location: 'cutoutA3', + }, + [stagingArea2]: { + name: 'stagingArea', + id: stagingArea2, + location: 'cutoutB3', + }, } as AdditionalEquipment expect(getUnusedStagingAreas(mockAdditionalEquipment, [])).toEqual([ @@ -27,7 +39,11 @@ describe('getUnusedStagingAreas', () => { it('returns false for unused staging area', () => { const stagingArea = 'stagingAreaId' const mockAdditionalEquipment = { - [stagingArea]: { name: 'stagingArea', id: stagingArea, location: 'A3' }, + [stagingArea]: { + name: 'stagingArea', + id: stagingArea, + location: 'cutoutA3', + }, } as AdditionalEquipment const mockCommand = ([ { diff --git a/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedTrash.test.ts b/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedTrash.test.ts index 474a2f9d733..3d66dec7c66 100644 --- a/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedTrash.test.ts +++ b/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedTrash.test.ts @@ -2,6 +2,7 @@ import { FLEX_TRASH_DEF_URI } from '../../../../constants' import { getUnusedTrash } from '../getUnusedTrash' import type { CreateCommand } from '@opentrons/shared-data' import type { InitialDeckSetup } from '../../../../step-forms' +import type { AdditionalEquipment } from '../../FileSidebar' describe('getUnusedTrash', () => { it('returns true for unused trash bin', () => { @@ -10,7 +11,7 @@ describe('getUnusedTrash', () => { [labwareId]: { labwareDefURI: FLEX_TRASH_DEF_URI, id: labwareId }, } as unknown) as InitialDeckSetup['labware'] - expect(getUnusedTrash(mockTrash, [])).toEqual({ + expect(getUnusedTrash(mockTrash, {}, [])).toEqual({ trashBinUnused: true, wasteChuteUnused: false, }) @@ -29,10 +30,48 @@ describe('getUnusedTrash', () => { }, ] as unknown) as CreateCommand[] - expect(getUnusedTrash(mockTrash, mockCommand)).toEqual({ + expect(getUnusedTrash(mockTrash, {}, mockCommand)).toEqual({ trashBinUnused: true, wasteChuteUnused: false, }) }) - // TODO(jr, 10/30/23): add test coverage for waste chute + it('returns true for unused waste chute', () => { + const wasteChute = 'wasteChuteId' + const mockAdditionalEquipment = { + [wasteChute]: { + name: 'wasteChute', + id: wasteChute, + location: 'cutoutD3', + }, + } as AdditionalEquipment + expect(getUnusedTrash({}, mockAdditionalEquipment, [])).toEqual({ + trashBinUnused: false, + wasteChuteUnused: true, + }) + }) + it('returns false for unused waste chute', () => { + const wasteChute = 'wasteChuteId' + const mockAdditionalEquipment = { + [wasteChute]: { + name: 'wasteChute', + id: wasteChute, + location: 'cutoutD3', + }, + } as AdditionalEquipment + const mockCommand = ([ + { + labwareId: { + commandType: 'moveToAddressableArea', + params: { + pipetteId: 'mockId', + addressableAreaName: '1and8ChannelWasteChute', + }, + }, + }, + ] as unknown) as CreateCommand[] + expect(getUnusedTrash({}, mockAdditionalEquipment, mockCommand)).toEqual({ + trashBinUnused: false, + wasteChuteUnused: true, + }) + }) }) diff --git a/protocol-designer/src/components/FileSidebar/utils/getUnusedStagingAreas.ts b/protocol-designer/src/components/FileSidebar/utils/getUnusedStagingAreas.ts index a701858a9eb..ea68068390c 100644 --- a/protocol-designer/src/components/FileSidebar/utils/getUnusedStagingAreas.ts +++ b/protocol-designer/src/components/FileSidebar/utils/getUnusedStagingAreas.ts @@ -1,11 +1,12 @@ -import type { CreateCommand } from '@opentrons/shared-data' +import { getStagingAreaAddressableAreas } from '../../../utils' +import type { CreateCommand, CutoutId } from '@opentrons/shared-data' import type { AdditionalEquipment } from '../FileSidebar' export const getUnusedStagingAreas = ( additionalEquipment: AdditionalEquipment, commands?: CreateCommand[] ): string[] => { - const stagingAreaSlots = Object.values(additionalEquipment) + const stagingAreaCutoutIds = Object.values(additionalEquipment) .filter(equipment => equipment?.name === 'stagingArea') .map(equipment => { if (equipment.location == null) { @@ -16,19 +17,12 @@ export const getUnusedStagingAreas = ( return equipment.location ?? '' }) - const corresponding4thColumnSlots = stagingAreaSlots.map(slot => { - const letter = slot.charAt(0) - const correspondingLocation = stagingAreaSlots.find(slot => - slot.startsWith(letter) - ) - if (correspondingLocation) { - return letter + '4' - } - - return slot - }) + const stagingAreaAddressableAreaNames = getStagingAreaAddressableAreas( + // TODO(jr, 11/13/23): fix AdditionalEquipment['location'] from type string to CutoutId + stagingAreaCutoutIds as CutoutId[] + ) - const stagingAreaCommandSlots: string[] = corresponding4thColumnSlots.filter( + const stagingAreaCommandSlots: string[] = stagingAreaAddressableAreaNames.filter( location => commands?.filter( command => diff --git a/protocol-designer/src/components/FileSidebar/utils/getUnusedTrash.ts b/protocol-designer/src/components/FileSidebar/utils/getUnusedTrash.ts index 2585b2c75eb..90cf88e6ada 100644 --- a/protocol-designer/src/components/FileSidebar/utils/getUnusedTrash.ts +++ b/protocol-designer/src/components/FileSidebar/utils/getUnusedTrash.ts @@ -11,9 +11,9 @@ interface UnusedTrash { wasteChuteUnused: boolean } -// TODO(jr, 10/30/23): plug in waste chute logic when we know the commands! export const getUnusedTrash = ( labwareOnDeck: InitialDeckSetup['labware'], + additionalEquipment: InitialDeckSetup['additionalEquipmentOnDeck'], commands?: CreateCommand[] ): UnusedTrash => { const trashBin = Object.values(labwareOnDeck).find( @@ -21,7 +21,6 @@ export const getUnusedTrash = ( labware.labwareDefURI === FLEX_TRASH_DEF_URI || labware.labwareDefURI === OT_2_TRASH_DEF_URI ) - const hasTrashBinCommands = trashBin != null ? commands?.some( @@ -32,8 +31,22 @@ export const getUnusedTrash = ( command.params.labwareId === trashBin.id) ) : null + const wasteChute = Object.values(additionalEquipment).find( + aE => aE.name === 'wasteChute' + ) + const hasWasteChuteCommands = + wasteChute != null + ? commands?.some( + command => + command.commandType === 'moveToAddressableArea' && + (command.params.addressableAreaName === '1and8ChannelWasteChute' || + command.params.addressableAreaName === 'gripperWasteChute' || + command.params.addressableAreaName === '96ChannelWasteChute') + ) + : null + return { trashBinUnused: trashBin != null && !hasTrashBinCommands, - wasteChuteUnused: false, + wasteChuteUnused: wasteChute != null && !hasWasteChuteCommands, } } diff --git a/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx b/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx index 1879c63a317..7720a52871e 100644 --- a/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/LabwareLocationField/index.tsx @@ -1,6 +1,9 @@ import * as React from 'react' import { useSelector } from 'react-redux' -import { getModuleDisplayName, WASTE_CHUTE_SLOT } from '@opentrons/shared-data' +import { + getModuleDisplayName, + WASTE_CHUTE_CUTOUT, +} from '@opentrons/shared-data' import { i18n } from '../../../../localization' import { getAdditionalEquipmentEntities, @@ -37,7 +40,8 @@ export function LabwareLocationField( if (isLabwareOffDeck && hasWasteChute) { unoccupiedLabwareLocationsOptions = unoccupiedLabwareLocationsOptions.filter( - option => option.value !== 'offDeck' && option.value !== WASTE_CHUTE_SLOT + option => + option.value !== 'offDeck' && option.value !== WASTE_CHUTE_CUTOUT ) } else if (useGripper || isLabwareOffDeck) { unoccupiedLabwareLocationsOptions = unoccupiedLabwareLocationsOptions.filter( diff --git a/protocol-designer/src/components/labware/__tests__/utils.test.ts b/protocol-designer/src/components/labware/__tests__/utils.test.ts index bda5fc71096..b5c63951b1b 100644 --- a/protocol-designer/src/components/labware/__tests__/utils.test.ts +++ b/protocol-designer/src/components/labware/__tests__/utils.test.ts @@ -1,4 +1,4 @@ -import { WASTE_CHUTE_SLOT } from '@opentrons/shared-data' +import { WASTE_CHUTE_CUTOUT } from '@opentrons/shared-data' import { getHasWasteChute } from '..' import type { AdditionalEquipmentEntities } from '@opentrons/step-generation' @@ -25,7 +25,7 @@ describe('getHasWasteChute', () => { mockId2: { id: mockId2, name: 'wasteChute', - location: WASTE_CHUTE_SLOT, + location: WASTE_CHUTE_CUTOUT, }, } as AdditionalEquipmentEntities const result = getHasWasteChute(mockAdditionalEquipmentEntities) diff --git a/protocol-designer/src/components/labware/utils.ts b/protocol-designer/src/components/labware/utils.ts index f3f27add66d..1c92ee47186 100644 --- a/protocol-designer/src/components/labware/utils.ts +++ b/protocol-designer/src/components/labware/utils.ts @@ -1,7 +1,7 @@ import reduce from 'lodash/reduce' import { AdditionalEquipmentEntities, AIR } from '@opentrons/step-generation' import { WellFill } from '@opentrons/components' -import { WASTE_CHUTE_SLOT } from '@opentrons/shared-data' +import { WASTE_CHUTE_CUTOUT } from '@opentrons/shared-data' import { swatchColors, MIXED_WELL_COLOR } from '../swatchColors' import { ContentsByWell, WellContents } from '../../labware-ingred/types' @@ -40,7 +40,7 @@ export const getHasWasteChute = ( ): boolean => { return Object.values(additionalEquipmentEntities).some( additionalEquipmentEntity => - additionalEquipmentEntity.location === WASTE_CHUTE_SLOT && + additionalEquipmentEntity.location === WASTE_CHUTE_CUTOUT && additionalEquipmentEntity.name === 'wasteChute' ) } diff --git a/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx b/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx index 512682dd041..b692d4b2cfd 100644 --- a/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx +++ b/protocol-designer/src/components/modals/AnnouncementModal/announcements.tsx @@ -6,6 +6,7 @@ import { JUSTIFY_SPACE_AROUND, SPACING, } from '@opentrons/components' + import styles from './AnnouncementModal.css' export interface Announcement { @@ -219,4 +220,28 @@ export const announcements: Announcement[] = [ ), }, + { + announcementKey: 'deckConfigAnd96Channel8.0', + image: ( + + + + ), + heading: "We've updated the Protocol Designer", + message: ( + <> +

    + Introducing the Protocol Designer 8.0 with deck configuration and + 96-channel pipette support! +

    +

    + All protocols now require Opentrons App version + 7.1+ to run. +

    + + ), + }, ] diff --git a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx index 3d1bd093319..0acd1df9c28 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/EquipmentOption.tsx @@ -10,7 +10,10 @@ import { COLORS, StyleProps, TYPOGRAPHY, + useHoverTooltip, + Tooltip, } from '@opentrons/components' +import { i18n } from '../../../localization' interface EquipmentOptionProps extends StyleProps { onClick: React.MouseEventHandler @@ -30,45 +33,60 @@ export function EquipmentOption(props: EquipmentOptionProps): JSX.Element { disabled = false, ...styleProps } = props + + const [targetProps, tooltipProps] = useHoverTooltip() + return ( - - {showCheckbox ? ( - - ) : null} + <> - {image} + {showCheckbox ? ( + + ) : null} + + {image} + + + {text} + - - {text} - - + {disabled ? ( + + {i18n.t('tooltip.disabled_no_space_additional_items')} + + ) : null} + ) } diff --git a/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx index 24f11c7b5c7..423b0c93689 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx @@ -13,54 +13,53 @@ import { } from '@opentrons/components' import { OT2_ROBOT_TYPE, - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, + SINGLE_RIGHT_SLOT_FIXTURE, + STAGING_AREA_CUTOUTS, + STAGING_AREA_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import { i18n } from '../../../localization' import { getEnableDeckModification } from '../../../feature-flags/selectors' import { GoBack } from './GoBack' import { HandleEnter } from './HandleEnter' -import type { Cutout, DeckConfiguration } from '@opentrons/shared-data' +import type { DeckConfiguration, CutoutId } from '@opentrons/shared-data' import type { WizardTileProps } from './types' -const STAGING_AREA_SLOTS: Cutout[] = ['A3', 'B3', 'C3', 'D3'] - export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { const { values, goBack, proceed, setFieldValue } = props const isOt2 = values.fields.robotType === OT2_ROBOT_TYPE const deckConfigurationFF = useSelector(getEnableDeckModification) const stagingAreaItems = values.additionalEquipment.filter(equipment => - equipment.includes(STAGING_AREA_LOAD_NAME) + // TODO(bc, 11/14/2023): refactor the additional items field to include a cutoutId + // and a cutoutFixtureId so that we don't have to string parse here to generate them + equipment.includes('stagingArea') ) - const savedStagingAreaSlots = stagingAreaItems.flatMap(item => { - const [loadName, fixtureLocation] = item.split('_') - const fixtureId = `id_${fixtureLocation}` - return [ - { - fixtureId, - fixtureLocation, - loadName, - }, - ] as DeckConfiguration - }) + const savedStagingAreaSlots: DeckConfiguration = stagingAreaItems.flatMap( + item => { + // TODO(bc, 11/14/2023): refactor the additional items field to include a cutoutId + // and a cutoutFixtureId so that we don't have to string parse here to generate them + const cutoutId = item.split('_')[1] as CutoutId + return [ + { + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + cutoutId, + }, + ] + } + ) - // NOTE: fixtureId doesn't matter since we don't create - // the entity until you complete the create file wizard via createDeckFixture action - // fixtureId here is only needed to visually add to the deck configurator - const STANDARD_EMPTY_SLOTS: DeckConfiguration = STAGING_AREA_SLOTS.map( - fixtureLocation => ({ - fixtureId: `id_${fixtureLocation}`, - fixtureLocation, - loadName: STANDARD_SLOT_LOAD_NAME, + const STANDARD_EMPTY_SLOTS: DeckConfiguration = STAGING_AREA_CUTOUTS.map( + cutoutId => ({ + cutoutId, + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, }) ) STANDARD_EMPTY_SLOTS.forEach(emptySlot => { if ( !savedStagingAreaSlots.some( - slot => slot.fixtureLocation === emptySlot.fixtureLocation + ({ cutoutId }) => cutoutId === emptySlot.cutoutId ) ) { savedStagingAreaSlots.push(emptySlot) @@ -79,12 +78,12 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { return null } - const handleClickAdd = (fixtureLocation: string): void => { + const handleClickAdd = (cutoutId: string): void => { const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { - if (slot.fixtureLocation === fixtureLocation) { + if (slot.cutoutId === cutoutId) { return { ...slot, - loadName: STAGING_AREA_LOAD_NAME, + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, } } return slot @@ -92,16 +91,16 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { setUpdatedSlots(modifiedSlots) setFieldValue('additionalEquipment', [ ...values.additionalEquipment, - `${STAGING_AREA_LOAD_NAME}_${fixtureLocation}`, + `stagingArea_${cutoutId}`, ]) } - const handleClickRemove = (fixtureLocation: string): void => { + const handleClickRemove = (cutoutId: string): void => { const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { - if (slot.fixtureLocation === fixtureLocation) { + if (slot.cutoutId === cutoutId) { return { ...slot, - loadName: STANDARD_SLOT_LOAD_NAME, + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, } } return slot @@ -109,10 +108,7 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { setUpdatedSlots(modifiedSlots) setFieldValue( 'additionalEquipment', - without( - values.additionalEquipment, - `${STAGING_AREA_LOAD_NAME}_${fixtureLocation}` - ) + without(values.additionalEquipment, `stagingArea_${cutoutId}`) ) } diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx index fff9f9c446b..756142495e1 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/utils.test.tsx @@ -55,10 +55,10 @@ describe('getLastCheckedEquipment', () => { ...MOCK_FORM_STATE, additionalEquipment: [ 'trashBin', - 'stagingArea_A3', - 'stagingArea_B3', - 'stagingArea_C3', - 'stagingArea_D3', + 'stagingArea_cutoutA3', + 'stagingArea_cutoutB3', + 'stagingArea_cutoutC3', + 'stagingArea_cutoutD3', ], modulesByType: { ...MOCK_FORM_STATE.modulesByType, @@ -83,7 +83,7 @@ describe('getTrashSlot', () => { it('should return B3 when there is a staging area in slot A3', () => { MOCK_FORM_STATE = { ...MOCK_FORM_STATE, - additionalEquipment: ['stagingArea_A3'], + additionalEquipment: ['stagingArea_cutoutA3'], } const result = getTrashSlot(MOCK_FORM_STATE) expect(result).toBe('B3') diff --git a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx index 5bb0029d221..da77d891ae2 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx @@ -26,7 +26,7 @@ import { MAGNETIC_MODULE_V2, THERMOCYCLER_MODULE_V2, TEMPERATURE_MODULE_V2, - WASTE_CHUTE_SLOT, + WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import { actions as stepFormActions, @@ -238,7 +238,7 @@ export function CreateFileWizard(): JSX.Element | null { enableDeckModification && values.additionalEquipment.includes('wasteChute') ) { - dispatch(createDeckFixture('wasteChute', WASTE_CHUTE_SLOT)) + dispatch(createDeckFixture('wasteChute', WASTE_CHUTE_CUTOUT)) } // add staging areas const stagingAreas = values.additionalEquipment.filter(equipment => diff --git a/protocol-designer/src/components/modals/CreateFileWizard/types.ts b/protocol-designer/src/components/modals/CreateFileWizard/types.ts index 8eb99fa5dba..1c15b081f9a 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/types.ts +++ b/protocol-designer/src/components/modals/CreateFileWizard/types.ts @@ -10,10 +10,10 @@ export type AdditionalEquipment = | 'gripper' | 'wasteChute' | 'trashBin' - | 'stagingArea_A3' - | 'stagingArea_B3' - | 'stagingArea_C3' - | 'stagingArea_D3' + | 'stagingArea_cutoutA3' + | 'stagingArea_cutoutB3' + | 'stagingArea_cutoutC3' + | 'stagingArea_cutoutD3' export interface FormState { fields: NewProtocolFields pipettesByMount: FormPipettesByMount diff --git a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts index 84e5b585c99..ef367349783 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/utils.ts +++ b/protocol-designer/src/components/modals/CreateFileWizard/utils.ts @@ -4,6 +4,7 @@ import { TEMPERATURE_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' +import { COLUMN_3_SLOTS } from '../../../constants' import { OUTER_SLOTS_FLEX } from '../../../modules' import { isModuleWithCollisionIssue } from '../../modules' import { @@ -81,12 +82,14 @@ export const getTrashBinOptionDisabled = (values: FormState): boolean => { } export const getTrashSlot = (values: FormState): string => { - const stagingAreaLocations = values.additionalEquipment - .filter(equipment => equipment.includes('stagingArea')) - .map(stagingArea => stagingArea.split('_')[1]) + const stagingAddressableAreas = values.additionalEquipment.filter(equipment => + equipment.includes('stagingArea') + ) + const cutouts = stagingAddressableAreas.flatMap(aa => + COLUMN_3_SLOTS.filter(cutout => aa.includes(cutout)) + ) - // return default trash slot A3 if staging area is not on slot - if (!stagingAreaLocations.includes(FLEX_TRASH_DEFAULT_SLOT)) { + if (!cutouts.includes(FLEX_TRASH_DEFAULT_SLOT)) { return FLEX_TRASH_DEFAULT_SLOT } @@ -103,11 +106,8 @@ export const getTrashSlot = (values: FormState): string => { }, [] ) - const unoccupiedSlot = OUTER_SLOTS_FLEX.find( - slot => - !stagingAreaLocations.includes(slot.value) && - !moduleSlots.includes(slot.value) + slot => !cutouts.includes(slot.value) && !moduleSlots.includes(slot.value) ) if (unoccupiedSlot == null) { console.error( diff --git a/protocol-designer/src/components/modals/GateModal/index.tsx b/protocol-designer/src/components/modals/GateModal/index.tsx index df0fec43c71..eb70c0ed0f6 100644 --- a/protocol-designer/src/components/modals/GateModal/index.tsx +++ b/protocol-designer/src/components/modals/GateModal/index.tsx @@ -46,7 +46,7 @@ class GateModalComponent extends React.Component {

    • {i18n.t('card.body.data_collected_is_internal')}
    • - {/* TODO: BC 2018-09-26 uncomment when only using fullstory
    • {i18n.t('card.body.data_only_from_pd')}
    • */} +
    • {i18n.t('card.body.data_only_from_pd')}
    • {i18n.t('card.body.opt_out_of_data_collection')}
    diff --git a/protocol-designer/src/components/modules/AdditionalItemsRow.tsx b/protocol-designer/src/components/modules/AdditionalItemsRow.tsx index a6686a490f0..69672553acc 100644 --- a/protocol-designer/src/components/modules/AdditionalItemsRow.tsx +++ b/protocol-designer/src/components/modules/AdditionalItemsRow.tsx @@ -1,6 +1,9 @@ import * as React from 'react' import styled from 'styled-components' -import { WASTE_CHUTE_SLOT } from '@opentrons/shared-data' +import { + getCutoutDisplayName, + WASTE_CHUTE_CUTOUT, +} from '@opentrons/shared-data' import { OutlineButton, Flex, @@ -24,6 +27,8 @@ import { FlexSlotMap } from './FlexSlotMap' import styles from './styles.css' +import type { Cutout } from '@opentrons/shared-data' + interface AdditionalItemsRowProps { handleAttachment: () => void isEquipmentAdded: boolean @@ -97,9 +102,11 @@ export function AdditionalItemsRow(
    @@ -107,7 +114,7 @@ export function AdditionalItemsRow( selectedSlots={ name === 'trashBin' ? [trashBinSlot ?? ''] - : [WASTE_CHUTE_SLOT] + : [WASTE_CHUTE_CUTOUT] } />
    diff --git a/protocol-designer/src/components/modules/FlexSlotMap.tsx b/protocol-designer/src/components/modules/FlexSlotMap.tsx index 0f46be8f3a7..507656effb9 100644 --- a/protocol-designer/src/components/modules/FlexSlotMap.tsx +++ b/protocol-designer/src/components/modules/FlexSlotMap.tsx @@ -2,19 +2,22 @@ import * as React from 'react' import { FLEX_ROBOT_TYPE, getDeckDefFromRobotType, + getPositionFromSlotId, } from '@opentrons/shared-data' -import { RobotCoordinateSpace } from '@opentrons/components/src/hardware-sim/RobotCoordinateSpace' -import { DeckSlotLocation } from '@opentrons/components/src/hardware-sim/DeckSlotLocation' import { ALIGN_CENTER, BORDERS, COLORS, Flex, JUSTIFY_CENTER, + RobotCoordinateSpace, RobotCoordsForeignObject, + SingleSlotFixture, SPACING, } from '@opentrons/components' +import type { Cutout } from '@opentrons/shared-data' + const X_ADJUSTMENT_LEFT_SIDE = -101.5 const X_ADJUSTMENT = -17 const X_DIMENSION_MIDDLE_SLOTS = 160.3 @@ -44,20 +47,20 @@ export function FlexSlotMap(props: FlexSlotMapProps): JSX.Element { height="100px" viewBox={`${deckDef.cornerOffsetFromOrigin[0]} ${deckDef.cornerOffsetFromOrigin[1]} ${deckDef.dimensions[0]} ${deckDef.dimensions[1]}`} > - {deckDef.locations.orderedSlots.map(slotDef => ( - ( + ))} {selectedSlots.map((selectedSlot, index) => { - const slot = deckDef.locations.orderedSlots.find( - slot => slot.id === selectedSlot - ) - const [xSlotPosition = 0, ySlotPosition = 0] = slot?.position ?? [] + // if selected slot is passed as a cutout id, trim to define as slot id + const slotFromCutout = selectedSlot.replace('cutout', '') + const [xSlotPosition = 0, ySlotPosition = 0] = + getPositionFromSlotId(slotFromCutout, deckDef) ?? [] const isLeftSideofDeck = selectedSlot === 'A1' || @@ -84,7 +87,7 @@ export function FlexSlotMap(props: FlexSlotMapProps): JSX.Element { return ( getSlotIsEmpty(initialDeckSetup, slot) ) - const hasConflictedSlot = areSlotsEmpty.includes(false) + const hasWasteChute = + Object.values(initialDeckSetup.additionalEquipmentOnDeck).find( + aE => aE.name === 'wasteChute' + ) != null + const hasConflictedSlot = + hasWasteChute && values.selectedSlots.find(slot => slot === 'cutoutD3') + ? false + : areSlotsEmpty.includes(false) - const mappedStagingAreas = stagingAreas.flatMap(area => { - return [ - { - fixtureId: area.id, - fixtureLocation: area.location ?? '', - loadName: STAGING_AREA_LOAD_NAME, - }, - ] as DeckConfiguration + const mappedStagingAreas: DeckConfiguration = stagingAreas.flatMap(area => { + return area.location != null + ? [ + { + cutoutId: area.location as CutoutId, + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + }, + ] + : [] }) - const STANDARD_EMPTY_SLOTS: DeckConfiguration = STAGING_AREA_SLOTS.map( - fixtureLocation => ({ - fixtureId: `id_${fixtureLocation}`, - fixtureLocation: fixtureLocation as Cutout, - loadName: STANDARD_SLOT_LOAD_NAME, + const STANDARD_EMPTY_SLOTS: DeckConfiguration = STAGING_AREA_CUTOUTS.map( + cutoutId => ({ + cutoutId, + cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, }) ) STANDARD_EMPTY_SLOTS.forEach(emptySlot => { if ( !mappedStagingAreas.some( - slot => slot.fixtureLocation === emptySlot.fixtureLocation + ({ cutoutId }) => cutoutId === emptySlot.cutoutId ) ) { mappedStagingAreas.push(emptySlot) @@ -83,36 +89,33 @@ const StagingAreasModalComponent = ( selectableSlots ) - const handleClickAdd = (fixtureLocation: string): void => { + const handleClickAdd = (cutoutId: string): void => { const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { - if (slot.fixtureLocation === fixtureLocation) { + if (slot.cutoutId === cutoutId) { return { ...slot, - loadName: STAGING_AREA_LOAD_NAME, + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, } } return slot }) setUpdatedSlots(modifiedSlots) - const updatedSelectedSlots = [...values.selectedSlots, fixtureLocation] + const updatedSelectedSlots = [...values.selectedSlots, cutoutId] setFieldValue('selectedSlots', updatedSelectedSlots) } - const handleClickRemove = (fixtureLocation: string): void => { + const handleClickRemove = (cutoutId: string): void => { const modifiedSlots: DeckConfiguration = updatedSlots.map(slot => { - if (slot.fixtureLocation === fixtureLocation) { - return { - ...slot, - loadName: STANDARD_SLOT_LOAD_NAME, - } + if (slot.cutoutId === cutoutId) { + return { ...slot, cutoutFixtureId: SINGLE_RIGHT_SLOT_FIXTURE } } return slot }) setUpdatedSlots(modifiedSlots) - const updatedSelectedSlots = values.selectedSlots.filter( - item => item !== fixtureLocation + setFieldValue( + 'selectedSlots', + values.selectedSlots.filter(item => item !== cutoutId) ) - setFieldValue('selectedSlots', updatedSelectedSlots) } return ( diff --git a/protocol-designer/src/components/modules/StagingAreasRow.tsx b/protocol-designer/src/components/modules/StagingAreasRow.tsx index dd882953be1..7bdb2a88e78 100644 --- a/protocol-designer/src/components/modules/StagingAreasRow.tsx +++ b/protocol-designer/src/components/modules/StagingAreasRow.tsx @@ -11,6 +11,7 @@ import { TYPOGRAPHY, DIRECTION_ROW, } from '@opentrons/components' +import { getCutoutDisplayName } from '@opentrons/shared-data' import { i18n } from '../../localization' import stagingAreaImage from '../../images/staging_area.png' import { getStagingAreaSlots } from '../../utils' @@ -19,6 +20,7 @@ import { StagingAreasModal } from './StagingAreasModal' import { FlexSlotMap } from './FlexSlotMap' import styles from './styles.css' +import type { Cutout } from '@opentrons/shared-data' import type { AdditionalEquipmentEntity } from '@opentrons/step-generation' interface StagingAreasRowProps { @@ -60,12 +62,14 @@ export function StagingAreasRow(props: StagingAreasRowProps): JSX.Element { className={styles.module_col} style={{ marginLeft: SPACING.spacing32 }} /> - {hasStagingAreas ? ( + {hasStagingAreas && stagingAreaLocations != null ? ( <>
    + getCutoutDisplayName(location as Cutout) + )}`} />
    diff --git a/protocol-designer/src/components/modules/TrashModal.tsx b/protocol-designer/src/components/modules/TrashModal.tsx index fedb02bc905..f9e33cd75be 100644 --- a/protocol-designer/src/components/modules/TrashModal.tsx +++ b/protocol-designer/src/components/modules/TrashModal.tsx @@ -21,7 +21,7 @@ import { import { FLEX_ROBOT_TYPE, getDeckDefFromRobotType, - WASTE_CHUTE_SLOT, + WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import { i18n } from '../../localization' import { OUTER_SLOTS_FLEX } from '../../modules' @@ -144,7 +144,7 @@ export const TrashModal = (props: TrashModalProps): JSX.Element => { }) ) } else if (trashName === 'wasteChute') { - dispatch(createDeckFixture('wasteChute', WASTE_CHUTE_SLOT)) + dispatch(createDeckFixture('wasteChute', WASTE_CHUTE_CUTOUT)) } onCloseClick() @@ -154,7 +154,7 @@ export const TrashModal = (props: TrashModalProps): JSX.Element => { diff --git a/protocol-designer/src/components/modules/__tests__/AdditionalItemsRow.test.tsx b/protocol-designer/src/components/modules/__tests__/AdditionalItemsRow.test.tsx index 38221bf9321..19801fbd4e2 100644 --- a/protocol-designer/src/components/modules/__tests__/AdditionalItemsRow.test.tsx +++ b/protocol-designer/src/components/modules/__tests__/AdditionalItemsRow.test.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import i18n from 'i18next' import { renderWithProviders } from '@opentrons/components' -import { WASTE_CHUTE_SLOT } from '@opentrons/shared-data' import { Portal } from '../../portals/TopPortal' import { AdditionalItemsRow } from '../AdditionalItemsRow' @@ -71,7 +70,7 @@ describe('AdditionalItemsRow', () => { getByAltText('Waste Chute') getByText('mock slot map') getByText('Position:') - getByText(`Slot ${WASTE_CHUTE_SLOT}`) + getByText('D3') getByRole('button', { name: 'remove' }).click() expect(props.handleAttachment).toHaveBeenCalled() }) diff --git a/protocol-designer/src/components/modules/__tests__/ModuleRow.test.tsx b/protocol-designer/src/components/modules/__tests__/ModuleRow.test.tsx index ebb5ab60f17..2b240aae6ef 100644 --- a/protocol-designer/src/components/modules/__tests__/ModuleRow.test.tsx +++ b/protocol-designer/src/components/modules/__tests__/ModuleRow.test.tsx @@ -151,7 +151,7 @@ describe('ModuleRow', () => { ).toBe('GEN1') expect( wrapper.find(LabeledValue).filter({ label: 'Position' }).prop('value') - ).toBe('Slot 1') + ).toBe('1') }) it('does not display module model and slot when module has not been added to protocol', () => { diff --git a/protocol-designer/src/components/modules/__tests__/StagingAreasRow.test.tsx b/protocol-designer/src/components/modules/__tests__/StagingAreasRow.test.tsx index baa7ad21fbe..40e74342714 100644 --- a/protocol-designer/src/components/modules/__tests__/StagingAreasRow.test.tsx +++ b/protocol-designer/src/components/modules/__tests__/StagingAreasRow.test.tsx @@ -42,7 +42,7 @@ describe('StagingAreasRow', () => { const { getByRole, getByText } = render(props) getByText('mock slot map') getByText('Position:') - getByText('Slots B3') + getByText('B3') getByRole('button', { name: 'remove' }).click() expect(props.handleAttachment).toHaveBeenCalled() getByRole('button', { name: 'edit' }).click() diff --git a/protocol-designer/src/components/modules/__tests__/TrashModal.test.tsx b/protocol-designer/src/components/modules/__tests__/TrashModal.test.tsx index a498f587784..04c665c46b7 100644 --- a/protocol-designer/src/components/modules/__tests__/TrashModal.test.tsx +++ b/protocol-designer/src/components/modules/__tests__/TrashModal.test.tsx @@ -12,7 +12,7 @@ import { } from '../../../labware-ingred/actions' import { FLEX_TRASH_DEF_URI } from '../../../constants' import { TrashModal } from '../TrashModal' -import { WASTE_CHUTE_SLOT } from '@opentrons/shared-data' +import { WASTE_CHUTE_CUTOUT } from '@opentrons/shared-data' jest.mock('../../../step-forms') jest.mock('../../../step-forms/selectors') @@ -114,7 +114,7 @@ describe('TrashModal ', () => { await waitFor(() => { expect(mockCreateDeckFixture).toHaveBeenCalledWith( 'wasteChute', - WASTE_CHUTE_SLOT + WASTE_CHUTE_CUTOUT ) }) }) diff --git a/protocol-designer/src/components/modules/utils.ts b/protocol-designer/src/components/modules/utils.ts index ead37eaedf6..06491f2cadb 100644 --- a/protocol-designer/src/components/modules/utils.ts +++ b/protocol-designer/src/components/modules/utils.ts @@ -1,6 +1,5 @@ import { MODULES_WITH_COLLISION_ISSUES } from '@opentrons/step-generation' import { ModuleModel } from '@opentrons/shared-data' export function isModuleWithCollisionIssue(model: ModuleModel): boolean { - // @ts-expect-error(sa, 2021-6-21): ModuleModel is a super type of the elements in MODULES_WITH_COLLISION_ISSUES return MODULES_WITH_COLLISION_ISSUES.includes(model) } diff --git a/protocol-designer/src/components/steplist/MoveLabwareHeader.tsx b/protocol-designer/src/components/steplist/MoveLabwareHeader.tsx index 9d641a34362..bfba5d12886 100644 --- a/protocol-designer/src/components/steplist/MoveLabwareHeader.tsx +++ b/protocol-designer/src/components/steplist/MoveLabwareHeader.tsx @@ -6,7 +6,7 @@ import { Tooltip, useHoverTooltip, TOOLTIP_FIXED } from '@opentrons/components' import { getLabwareDisplayName, getModuleDisplayName, - WASTE_CHUTE_SLOT, + WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import { getAdditionalEquipmentEntities, @@ -59,7 +59,7 @@ export function MoveLabwareHeader(props: MoveLabwareHeaderProps): JSX.Element { destSlot = getLabwareDisplayName(labwareEntities[destinationSlot].def) } else if ( getHasWasteChute(additionalEquipmentEntities) && - destinationSlot === WASTE_CHUTE_SLOT + destinationSlot === WASTE_CHUTE_CUTOUT ) { destSlot = i18n.t('application.waste_chute_slot') } else { diff --git a/protocol-designer/src/constants.ts b/protocol-designer/src/constants.ts index 92b2ba2d5df..e5b5bb6fe4e 100644 --- a/protocol-designer/src/constants.ts +++ b/protocol-designer/src/constants.ts @@ -159,3 +159,5 @@ export const THERMOCYCLER_PROFILE: 'thermocyclerProfile' = 'thermocyclerProfile' export const OT_2_TRASH_DEF_URI = 'opentrons/opentrons_1_trash_1100ml_fixed/1' export const FLEX_TRASH_DEF_URI = 'opentrons/opentrons_1_trash_3200ml_fixed/1' + +export const COLUMN_3_SLOTS = ['A3', 'B3', 'C3', 'D3'] diff --git a/protocol-designer/src/file-data/selectors/fileCreator.ts b/protocol-designer/src/file-data/selectors/fileCreator.ts index ed5aaa56638..89eba6b230e 100644 --- a/protocol-designer/src/file-data/selectors/fileCreator.ts +++ b/protocol-designer/src/file-data/selectors/fileCreator.ts @@ -41,16 +41,13 @@ import type { LabwareEntities, PipetteEntities, RobotState, - AdditionalEquipmentEntity, } from '@opentrons/step-generation' import type { CommandAnnotationV1Mixin, CommandV8Mixin, CreateCommand, - Cutout, LabwareV2Mixin, LiquidV1Mixin, - LoadFixtureCreateCommand, LoadLabwareCreateCommand, LoadModuleCreateCommand, LoadPipetteCreateCommand, @@ -300,30 +297,6 @@ export const createFile: Selector = createSelector( [] ) - // TODO(jr, 10/31/23): update to loadAddressableArea - const loadFixtureCommands = reduce< - AdditionalEquipmentEntity, - LoadFixtureCreateCommand[] - >( - Object.values(additionalEquipmentEntities), - (acc, additionalEquipment): LoadFixtureCreateCommand[] => { - if (additionalEquipment.name === 'gripper') return acc - - const loadFixtureCommands = { - key: uuid(), - commandType: 'loadFixture' as const, - params: { - fixtureId: additionalEquipment.id, - location: { cutout: additionalEquipment.location as Cutout }, - loadName: additionalEquipment.name, - }, - } - - return [...acc, loadFixtureCommands] - }, - [] - ) - const loadLiquidCommands = getLoadLiquidCommands( ingredients, ingredLocations @@ -356,7 +329,6 @@ export const createFile: Selector = createSelector( labwareDefsByURI ) const loadCommands: CreateCommand[] = [ - ...loadFixtureCommands, ...loadPipetteCommands, ...loadModuleCommands, ...loadAdapterCommands, diff --git a/protocol-designer/src/images/deck_configuration.png b/protocol-designer/src/images/deck_configuration.png new file mode 100644 index 00000000000..5a58b5f26e9 Binary files /dev/null and b/protocol-designer/src/images/deck_configuration.png differ diff --git a/protocol-designer/src/images/modules/heater_shaker_module_transparent.png b/protocol-designer/src/images/modules/heater_shaker_module_transparent.png new file mode 100644 index 00000000000..349024bbcf7 Binary files /dev/null and b/protocol-designer/src/images/modules/heater_shaker_module_transparent.png differ diff --git a/protocol-designer/src/images/modules/temp_deck_gen_2_transparent.png b/protocol-designer/src/images/modules/temp_deck_gen_2_transparent.png new file mode 100644 index 00000000000..a5e506536bd Binary files /dev/null and b/protocol-designer/src/images/modules/temp_deck_gen_2_transparent.png differ diff --git a/protocol-designer/src/initialize.ts b/protocol-designer/src/initialize.ts index 8131c18f072..b69af44bc07 100644 --- a/protocol-designer/src/initialize.ts +++ b/protocol-designer/src/initialize.ts @@ -1,7 +1,6 @@ import { i18n } from './localization' import { selectors as loadFileSelectors } from './load-file' -import { selectors as analyticsSelectors } from './analytics' -import { initializeFullstory } from './analytics/fullstory' + export const initialize = (store: Record): void => { if (process.env.NODE_ENV === 'production') { window.onbeforeunload = (_e: unknown) => { @@ -10,10 +9,5 @@ export const initialize = (store: Record): void => { ? i18n.t('alert.window.confirm_leave') : undefined } - - // Initialize analytics if user has already opted in - if (analyticsSelectors.getHasOptedIn(store.getState())) { - initializeFullstory() - } } } diff --git a/protocol-designer/src/labware-ingred/reducers/index.ts b/protocol-designer/src/labware-ingred/reducers/index.ts index a26e5b1b95f..4d0fd60f10f 100644 --- a/protocol-designer/src/labware-ingred/reducers/index.ts +++ b/protocol-designer/src/labware-ingred/reducers/index.ts @@ -232,6 +232,8 @@ export const savedLabware: Reducer = handleActions( slot = location.moduleId } else if ('labwareId' in location) { slot = location.labwareId + } else if ('addressableAreaName' in location) { + slot = location.addressableAreaName } else { slot = location.slotName } diff --git a/protocol-designer/src/labware-ingred/utils.ts b/protocol-designer/src/labware-ingred/utils.ts index abb598e16ea..685e1bac8e7 100644 --- a/protocol-designer/src/labware-ingred/utils.ts +++ b/protocol-designer/src/labware-ingred/utils.ts @@ -8,7 +8,7 @@ export function getNextAvailableDeckSlot( robotType: RobotType ): DeckSlot | null | undefined { const deckDef = getDeckDefFromRobotType(robotType) - return deckDef.locations.orderedSlots.find(slot => + return deckDef.locations.addressableAreas.find(slot => getSlotIsEmpty(initialDeckSetup, slot.id) )?.id } diff --git a/protocol-designer/src/load-file/migration/7_0_0.ts b/protocol-designer/src/load-file/migration/7_0_0.ts index b3d61ef0466..3495556054a 100644 --- a/protocol-designer/src/load-file/migration/7_0_0.ts +++ b/protocol-designer/src/load-file/migration/7_0_0.ts @@ -4,6 +4,7 @@ import { getOnlyLatestDefs } from '../../labware-defs' import { INITIAL_DECK_SETUP_STEP_ID } from '../../constants' import { getAdapterAndLabwareSplitInfo } from './utils/getAdapterAndLabwareSplitInfo' import type { + LabwareDefinition2, LabwareDefinitionsByUri, ProtocolFileV6, } from '@opentrons/shared-data' @@ -31,6 +32,23 @@ interface LabwareLocationUpdate { [id: string]: string } +const ADAPTER_LABWARE_COMBO_LOAD_NAMES = [ + 'opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep', + 'opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat', + 'opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt', + 'opentrons_universal_flat_adapter_corning_384_wellplate_112ul_flat', + 'opentrons_96_aluminumblock_biorad_wellplate_200ul', + 'opentrons_96_aluminumblock_nest_wellplate_100ul', +] + +interface LabwareIdMapping { + [oldLabwareAdapterComboId: string]: { + newLabwareId: string + newAdapterId: string + newLabwareDefinitionUri: string + } +} + export const migrateFile = ( appData: ProtocolFileV6 ): ProtocolFile => { @@ -46,23 +64,36 @@ export const migrateFile = ( const getIsAdapter = (labwareId: string): boolean => { const labwareEntity = labware[labwareId] - if (labwareEntity == null) return false + if (labwareEntity == null) { + console.error( + `expected to find labware entity with labwareId ${labwareId} but could not` + ) + return false + } const loadName = labwareDefinitions[labwareEntity.definitionId].parameters.loadName - - return ( - loadName === 'opentrons_96_deep_well_adapter_nest_wellplate_2ml_deep' || - loadName === - 'opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat' || - loadName === - 'opentrons_96_pcr_adapter_nest_wellplate_100ul_pcr_full_skirt' || - loadName === - 'opentrons_universal_flat_adapter_corning_384_wellplate_112ul_flat' || - loadName === 'opentrons_96_aluminumblock_biorad_wellplate_200ul' || - loadName === 'opentrons_96_aluminumblock_nest_wellplate_100ul' - ) + return ADAPTER_LABWARE_COMBO_LOAD_NAMES.includes(loadName) } + const mappedLabwareIds = Object.keys(labware) + .filter(labwareId => getIsAdapter(labwareId)) + .reduce((acc: LabwareIdMapping, labwareId: string): LabwareIdMapping => { + const { labwareUri, adapterUri } = getAdapterAndLabwareSplitInfo( + labwareId + ) + const newLabwareId = `${uuid()}:${labwareUri}` + const newAdapterId = `${uuid()}:${adapterUri}` + + return { + ...acc, + [labwareId]: { + newLabwareId, + newAdapterId, + newLabwareDefinitionUri: labwareUri, + }, + } + }, {}) + const loadPipetteCommands: LoadPipetteCreateCommand[] = commands .filter( (command): command is LoadPipetteCommandV6 => @@ -102,7 +133,6 @@ export const migrateFile = ( adapterDisplayName, labwareDisplayName, } = getAdapterAndLabwareSplitInfo(command.params.labwareId) - const previousLabwareIdUuid = command.params.labwareId.split(':')[0] const labwareLocation = command.params.location let adapterLocation: LabwareLocation = 'offDeck' if (labwareLocation === 'offDeck') { @@ -112,21 +142,24 @@ export const migrateFile = ( } else if ('slotName' in labwareLocation) { adapterLocation = { slotName: labwareLocation.slotName } } - const defUris = Object.keys(allLatestDefs) - const adapterDefUri = defUris.find(defUri => defUri === adapterUri) ?? '' - const labwareDefUri = defUris.find(defUri => defUri === labwareUri) ?? '' - const adapterLoadname = allLatestDefs[adapterDefUri].parameters.loadName - const labwareLoadname = allLatestDefs[labwareDefUri].parameters.loadName - const adapterId = `${uuid()}:${adapterUri}` + const { + parameters: adapterParameters, + version: adapterVersion, + } = allLatestDefs[adapterUri] + const { + parameters: labwareParameters, + version: labwareVersion, + } = allLatestDefs[labwareUri] + const adapterId = mappedLabwareIds[command.params.labwareId].newAdapterId const loadAdapterCommand: LoadLabwareCreateCommand = { key: uuid(), commandType: 'loadLabware', params: { labwareId: adapterId, - loadName: adapterLoadname, + loadName: adapterParameters.loadName, namespace: 'opentrons', - version: 1, + version: adapterVersion, location: adapterLocation, displayName: adapterDisplayName, }, @@ -136,11 +169,10 @@ export const migrateFile = ( key: uuid(), commandType: 'loadLabware', params: { - // keeping same Uuid as previous id for ingredLocation and savedStepForms mapping - labwareId: `${previousLabwareIdUuid}:${labwareUri}`, - loadName: labwareLoadname, + labwareId: mappedLabwareIds[command.params.labwareId].newLabwareId, + loadName: labwareParameters.loadName, namespace: 'opentrons', - version: 1, + version: labwareVersion, location: { labwareId: adapterId }, displayName: labwareDisplayName, }, @@ -148,18 +180,24 @@ export const migrateFile = ( return [loadAdapterCommand, loadLabwareCommand] }) - const newLabwareDefinitions: LabwareDefinitionsByUri = Object.keys( labwareDefinitions ).reduce((acc: LabwareDefinitionsByUri, defId: string) => { - if (!getIsAdapter(defId)) { - acc[defId] = labwareDefinitions[defId] - } else { + const labwareDefinition = labwareDefinitions[defId] + if (labwareDefinition == null) { + console.error( + `expected to find matching labware definition with definitionURI ${defId} but could not` + ) + } + const loadName = labwareDefinition.parameters.loadName + if (ADAPTER_LABWARE_COMBO_LOAD_NAMES.includes(loadName)) { const { adapterUri, labwareUri } = getAdapterAndLabwareSplitInfo(defId) const adapterLabwareDef = allLatestDefs[adapterUri] const labwareDef = allLatestDefs[labwareUri] acc[adapterUri] = adapterLabwareDef acc[labwareUri] = labwareDef + } else { + acc[defId] = labwareDefinitions[defId] } return acc }, {}) @@ -173,7 +211,9 @@ export const migrateFile = ( .map(command => { const labwareId = command.params.labwareId const definitionId = labware[labwareId].definitionId - const { namespace, version } = labwareDefinitions[definitionId] + const { namespace, version, parameters } = labwareDefinitions[ + definitionId + ] const labwareLocation = command.params.location let location: LabwareLocation = 'offDeck' if (labwareLocation === 'offDeck') { @@ -188,7 +228,7 @@ export const migrateFile = ( ...command, params: { ...command.params, - loadName: definitionId, + loadName: parameters.loadName, namespace, version, location, @@ -239,13 +279,8 @@ export const migrateFile = ( if (ingredLocations == null) return {} for (const [labwareId, wellData] of Object.entries(ingredLocations)) { if (getIsAdapter(labwareId)) { - const labwareIdUuid = labwareId.split(':')[0] - const matchingCommand = loadAdapterAndLabwareCommands.find( - command => command.params.labwareId?.split(':')[0] === labwareIdUuid - ) - const updatedLabwareId = - matchingCommand != null ? matchingCommand.params.labwareId ?? '' : '' - updatedIngredLocations[updatedLabwareId] = wellData + const newLabwareId = mappedLabwareIds[labwareId].newLabwareId + updatedIngredLocations[newLabwareId] = wellData } else { updatedIngredLocations[labwareId] = wellData } @@ -259,18 +294,63 @@ export const migrateFile = ( ): Record => { return mapValues(savedStepForms, stepForm => { if (stepForm.stepType === 'moveLiquid') { - const aspirateLabware = - newLabwareDefinitions[labware[stepForm.aspirate_labware].definitionId] - const aspirateTouchTipIncompatible = aspirateLabware?.parameters.quirks?.includes( + let newAspirateLabwareDefinition: LabwareDefinition2 | null = null + + let aspirateLabware = stepForm.aspirate_labware + // aspirate labware is an adapter/labware split + if (stepForm.aspirate_labware in mappedLabwareIds) { + const newLabwareDefUri = + mappedLabwareIds[stepForm.aspirate_labware].newLabwareDefinitionUri + + newAspirateLabwareDefinition = newLabwareDefinitions[newLabwareDefUri] + aspirateLabware = + mappedLabwareIds[stepForm.aspirate_labware].newLabwareId + // aspirate labware is just a labware and doesn't need to be mapped + } else { + newAspirateLabwareDefinition = + newLabwareDefinitions[ + labware[stepForm.aspirate_labware].definitionId + ] + } + if (newAspirateLabwareDefinition == null) { + console.error( + `expected to find aspirate labware definition with labwareId ${aspirateLabware} but could not` + ) + } + + const aspirateTouchTipIncompatible = newAspirateLabwareDefinition?.parameters.quirks?.includes( 'touchTipDisabled' ) - const dispenseLabware = - newLabwareDefinitions[labware[stepForm.dispense_labware].definitionId] - const dispenseTouchTipIncompatible = dispenseLabware?.parameters.quirks?.includes( + + let newDispenseLabwareDefinition: LabwareDefinition2 | null = null + + let dispenseLabware = stepForm.dispense_labware + // dispense labware is an adapter/labware split + if (stepForm.dispense_labware in mappedLabwareIds) { + const labwareUri = + mappedLabwareIds[stepForm.dispense_labware].newLabwareDefinitionUri + newDispenseLabwareDefinition = newLabwareDefinitions[labwareUri] + dispenseLabware = + mappedLabwareIds[stepForm.dispense_labware].newLabwareId + // dispense labware is just a labware and doesn't need to be mapped + } else { + newDispenseLabwareDefinition = + newLabwareDefinitions[ + labware[stepForm.dispense_labware].definitionId + ] + } + if (newDispenseLabwareDefinition == null) { + console.error( + `expected to find dispense labware definition with labwareId ${dispenseLabware} but could not` + ) + } + const dispenseTouchTipIncompatible = newDispenseLabwareDefinition?.parameters.quirks?.includes( 'touchTipDisabled' ) return { ...stepForm, + dispense_labware: dispenseLabware, + aspirate_labware: aspirateLabware, aspirate_touchTip_checkbox: aspirateTouchTipIncompatible ? false : stepForm.aspirate_touchTip_checkbox ?? false, @@ -285,13 +365,34 @@ export const migrateFile = ( : stepForm.dispense_touchTip_mmFromBottom ?? null, } } else if (stepForm.stepType === 'mix') { - const mixLabware = - newLabwareDefinitions[labware[stepForm.labware].definitionId] - const mixTouchTipIncompatible = mixLabware?.parameters.quirks?.includes( + let newMixLabwareDefinition: LabwareDefinition2 | null = null + + let mixLabware = stepForm.labware + // mix labware is an adapter/labware split + if (stepForm.labware in mappedLabwareIds) { + const labwareUri = + mappedLabwareIds[stepForm.labware].newLabwareDefinitionUri + newMixLabwareDefinition = newLabwareDefinitions[labwareUri] + mixLabware = mappedLabwareIds[stepForm.labware].newLabwareId + // mix labware is just a labware and doesn't need to be mapped + } else { + newMixLabwareDefinition = + newLabwareDefinitions[labware[stepForm.labware].definitionId] + } + + if (newMixLabwareDefinition == null) { + console.error( + `expected to find mix labware definition with labwareId ${mixLabware} but could not` + ) + } + + const mixTouchTipIncompatible = newMixLabwareDefinition?.parameters.quirks?.includes( 'touchTipDisabled' ) + return { ...stepForm, + labware: mixLabware, mix_touchTip_checkbox: mixTouchTipIncompatible ? false : stepForm.mix_touchTip_checkbox ?? false, diff --git a/protocol-designer/src/load-file/migration/8_0_0.ts b/protocol-designer/src/load-file/migration/8_0_0.ts index c397cea13e9..734f2f9ea46 100644 --- a/protocol-designer/src/load-file/migration/8_0_0.ts +++ b/protocol-designer/src/load-file/migration/8_0_0.ts @@ -37,13 +37,7 @@ interface LabwareLocationUpdate { export const migrateFile = ( appData: ProtocolFileV7 ): ProtocolFile => { - const { - designerApplication, - commands, - robot, - labwareDefinitions, - liquids, - } = appData + const { designerApplication, commands, robot, liquids } = appData if (designerApplication == null || designerApplication.data == null) { throw Error('The designerApplication key in your file is corrupt.') @@ -135,31 +129,6 @@ export const migrateFile = ( filteredSavedStepForms ) - const loadLabwareCommands: LoadLabwareCreateCommand[] = commands - .filter( - (command): command is LoadLabwareCreateCommand => - command.commandType === 'loadLabware' - ) - .map(command => { - // protocols that do multiple migrations through 7.0.0 have a loadName === definitionURI - // this ternary below fixes that - const loadName = - labwareDefinitions[command.params.loadName] != null - ? labwareDefinitions[command.params.loadName].parameters.loadName - : command.params.loadName - return { - ...command, - params: { - ...command.params, - loadName, - }, - } - }) - - const migratedCommandsV8 = commands.filter( - command => command.commandType !== 'loadLabware' - ) - const flexDeckSpec: OT3RobotMixin = { robot: { model: FLEX_ROBOT_TYPE, @@ -216,11 +185,7 @@ export const migrateFile = ( const commandv8Mixin: CommandV8Mixin = { commandSchemaId: 'opentronsCommandSchemaV8', - commands: [ - ...migratedCommandsV8, - ...loadLabwareCommands, - ...trashLoadCommand, - ], + commands: [...commands, ...trashLoadCommand], } const commandAnnotionaV1Mixin: CommandAnnotationV1Mixin = { diff --git a/protocol-designer/src/load-file/migration/__tests__/7_0_0.test.ts b/protocol-designer/src/load-file/migration/__tests__/7_0_0.test.ts index 4044825af70..b5c13c2af97 100644 --- a/protocol-designer/src/load-file/migration/__tests__/7_0_0.test.ts +++ b/protocol-designer/src/load-file/migration/__tests__/7_0_0.test.ts @@ -69,7 +69,7 @@ describe('v7 migration', () => { '0b44c760-75c7-11ea-b42f-4b64e50f43e5:opentrons/opentrons_96_tiprack_300ul/1', location: { slotName: '2' }, displayName: 'Opentrons 96 Tip Rack 300 µL', - loadName: 'opentrons/opentrons_96_tiprack_300ul/1', + loadName: 'opentrons_96_tiprack_300ul', namespace: 'opentrons', version: 1, }, @@ -84,7 +84,7 @@ describe('v7 migration', () => { moduleId: '0b419310-75c7-11ea-b42f-4b64e50f43e5:magneticModuleType', }, displayName: 'NEST 96 Well Plate 100 µL PCR Full Skirt', - loadName: 'opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1', + loadName: 'nest_96_wellplate_100ul_pcr_full_skirt', namespace: 'opentrons', version: 1, }, @@ -101,8 +101,7 @@ describe('v7 migration', () => { }, namespace: 'opentrons', version: 1, - loadName: - 'opentrons/opentrons_24_aluminumblock_generic_2ml_screwcap/1', + loadName: 'opentrons_24_aluminumblock_generic_2ml_screwcap', displayName: 'Opentrons 24 Well Aluminum Block with Generic 2 mL Screwcap', }, diff --git a/protocol-designer/src/load-file/migration/utils/getAdapterAndLabwareSplitInfo.ts b/protocol-designer/src/load-file/migration/utils/getAdapterAndLabwareSplitInfo.ts index 0587b9431b7..e531c6d7de0 100644 --- a/protocol-designer/src/load-file/migration/utils/getAdapterAndLabwareSplitInfo.ts +++ b/protocol-designer/src/load-file/migration/utils/getAdapterAndLabwareSplitInfo.ts @@ -54,8 +54,8 @@ export const getAdapterAndLabwareSplitInfo = ( labwareId.includes('opentrons_96_aluminumblock_biorad_wellplate_200ul') ) { return { - labwareUri: 'opentrons/opentrons_96_well_aluminum_block/1', - adapterUri: 'opentrons/biorad_96_wellplate_200ul_pcr/2', + adapterUri: 'opentrons/opentrons_96_well_aluminum_block/1', + labwareUri: 'opentrons/biorad_96_wellplate_200ul_pcr/2', labwareDisplayName: 'Bio-Rad 96 Well Plate 200 µL PCR', adapterDisplayName: 'Opentrons 96 Well Aluminum Block', } diff --git a/protocol-designer/src/localization/en/alert.json b/protocol-designer/src/localization/en/alert.json index 475e45012f2..d4412ea07a8 100644 --- a/protocol-designer/src/localization/en/alert.json +++ b/protocol-designer/src/localization/en/alert.json @@ -49,10 +49,10 @@ "title": "Missing labware", "body": "Your module has no labware on it. We recommend you add labware before proceeding." }, - "export_v7_protocol_7_0": { + "export_v8_protocol_7_1": { "title": "Robot and app update may be required", "body1": "This protocol can only run on app and robot server version", - "body2": "7.0 or higher", + "body2": "7.1 or higher", "body3": ". Please ensure your robot is updated to the correct version." }, "change_magnet_module_model": { @@ -163,6 +163,10 @@ "ADDITIONAL_EQUIPMENT_DOES_NOT_EXIST": { "title": "{{additionalEquipment}} does not exist", "body": "Attempting to interact with an unknown entity." + }, + "GRIPPER_REQUIRED": { + "title": "A gripper is required to complete this action", + "body": "Attempting to move a labware without a gripper into the waste chute. Please add a gripper to this step." } }, "warning": { diff --git a/protocol-designer/src/localization/en/tooltip.json b/protocol-designer/src/localization/en/tooltip.json index 72ebd3993b5..bcb0e2acff4 100644 --- a/protocol-designer/src/localization/en/tooltip.json +++ b/protocol-designer/src/localization/en/tooltip.json @@ -4,6 +4,7 @@ "disabled_cannot_delete_trash": "A Trash Bin or Waste Chute is required", "disabled_off_deck": "Off-deck labware cannot be modified unless on starting deck state.", "disabled_step_creation": "New steps cannot be added in Batch Edit mode.", + "disabled_no_space_additional_items": "No space for this combination of staging area slots and modules.", "not_in_beta": "ⓘ Coming Soon", "step_description": { diff --git a/protocol-designer/src/step-forms/reducers/index.ts b/protocol-designer/src/step-forms/reducers/index.ts index 82e0021ea29..70578bb9526 100644 --- a/protocol-designer/src/step-forms/reducers/index.ts +++ b/protocol-designer/src/step-forms/reducers/index.ts @@ -20,9 +20,10 @@ import { MAGNETIC_MODULE_V1, PipetteName, THERMOCYCLER_MODULE_TYPE, - LoadFixtureCreateCommand, - STANDARD_SLOT_LOAD_NAME, - TRASH_BIN_LOAD_NAME, + WASTE_CHUTE_ADDRESSABLE_AREAS, + getDeckDefFromRobotType, + AddressableAreaName, + CutoutId, } from '@opentrons/shared-data' import type { RootState as LabwareDefsRootState } from '../../labware-defs' import { rootReducer as labwareDefsRootReducer } from '../../labware-defs' @@ -43,6 +44,7 @@ import { getLabwareOnModule } from '../../ui/modules/utils' import { nestedCombineReducers } from './nestedCombineReducers' import { PROFILE_CYCLE, PROFILE_STEP } from '../../form-types' import { + COLUMN_4_SLOTS, NormalizedAdditionalEquipmentById, NormalizedPipetteById, } from '@opentrons/step-generation' @@ -1162,6 +1164,11 @@ export const labwareInvariantProperties: Reducer< labwareDef.namespace === namespace && labwareDef.version === version ) + if (labwareDefinitionMatch == null) { + console.error( + `expected to find labware definition match with loadname ${loadName} but could not` + ) + } const labwareDefURI = labwareDefinitionMatch != null ? labwareDefinitionMatch[0] : '' const id = labwareId ?? '' @@ -1323,58 +1330,111 @@ export const additionalEquipmentInvariantProperties = handleActions { const { file } = action.payload - const gripperCommands = Object.values(file.commands).filter( + const isFlex = file.robot.model === FLEX_ROBOT_TYPE + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) + const cutoutFixtures = deckDef.cutoutFixtures + const providesAddressableAreasForAddressableArea = cutoutFixtures.find( + cutoutFixture => cutoutFixture.id.includes('stagingAreaRightSlot') + )?.providesAddressableAreas + + const hasGripperCommands = Object.values(file.commands).some( (command): command is MoveLabwareCreateCommand => command.commandType === 'moveLabware' && command.params.strategy === 'usingGripper' ) - const fixtureCommands = Object.values(file.commands).filter( - (command): command is LoadFixtureCreateCommand => - command.commandType === 'loadFixture' + const hasWasteChuteCommands = Object.values(file.commands).some( + command => + (command.commandType === 'moveToAddressableArea' && + WASTE_CHUTE_ADDRESSABLE_AREAS.includes( + command.params.addressableAreaName + )) || + (command.commandType === 'moveLabware' && + command.params.newLocation !== 'offDeck' && + 'addressableAreaName' in command.params.newLocation && + WASTE_CHUTE_ADDRESSABLE_AREAS.includes( + command.params.addressableAreaName + )) ) - const fixtures = fixtureCommands.reduce( - ( - acc: NormalizedAdditionalEquipmentById, - command: LoadFixtureCreateCommand - ) => { - const { fixtureId, loadName, location } = command.params - const id = fixtureId ?? '' - if ( - loadName === STANDARD_SLOT_LOAD_NAME || - loadName === TRASH_BIN_LOAD_NAME - ) { - return acc - } - return { - ...acc, - [id]: { - id: id, - name: loadName, - location: location.cutout, + const wasteChuteId = `${uuid()}:wasteChute` + const wasteChute = hasWasteChuteCommands + ? { + [wasteChuteId]: { + name: 'wasteChute' as const, + id: wasteChuteId, + location: 'cutoutD3', }, } - }, - {} - ) - const hasGripper = gripperCommands.length > 0 - const isFlex = file.robot.model === FLEX_ROBOT_TYPE - const gripperId = `${uuid()}:gripper` - const gripper = { - [gripperId]: { - name: 'gripper' as const, - id: gripperId, - }, + : {} + + const getStagingAreaSlotNames = ( + commandType: 'moveLabware' | 'loadLabware', + locationKey: 'newLocation' | 'location' + ): AddressableAreaName[] => { + return Object.values(file.commands) + .filter( + command => + command.commandType === commandType && + command.params[locationKey] !== 'offDeck' && + 'slotName' in command.params[locationKey] && + COLUMN_4_SLOTS.includes(command.params[locationKey].slotName) + ) + .map(command => command.params[locationKey].slotName) } - if (isFlex) { - if (hasGripper) { - return { ...state, ...gripper, ...fixtures } - } else { - return { ...state, ...fixtures } + + const stagingAreaSlotNames = [ + ...new Set([ + ...getStagingAreaSlotNames('moveLabware', 'newLocation'), + ...getStagingAreaSlotNames('loadLabware', 'location'), + ]), + ] + + const findCutoutIdByAddressableArea = ( + addressableAreaName: AddressableAreaName + ): CutoutId | null => { + if (providesAddressableAreasForAddressableArea != null) { + for (const cutoutId in providesAddressableAreasForAddressableArea) { + if ( + providesAddressableAreasForAddressableArea[ + cutoutId as keyof typeof providesAddressableAreasForAddressableArea + ].includes(addressableAreaName) + ) { + return cutoutId as CutoutId + } + } + } + return null + } + + const stagingAreas = stagingAreaSlotNames.reduce((acc, slot) => { + const stagingAreaId = `${uuid()}:stagingArea` + const cutoutId = findCutoutIdByAddressableArea(slot) + return { + ...acc, + [stagingAreaId]: { + name: 'stagingArea' as const, + id: stagingAreaId, + location: cutoutId, + }, } + }, {}) + + const gripperId = `${uuid()}:gripper` + const gripper = hasGripperCommands + ? { + [gripperId]: { + name: 'gripper' as const, + id: gripperId, + }, + } + : {} + + if (isFlex) { + return { ...state, ...gripper, ...wasteChute, ...stagingAreas } } else { return { ...state } } }, + TOGGLE_IS_GRIPPER_REQUIRED: ( state: NormalizedAdditionalEquipmentById ): NormalizedAdditionalEquipmentById => { diff --git a/protocol-designer/src/step-forms/utils/index.ts b/protocol-designer/src/step-forms/utils/index.ts index b6b09bdeea7..139ff2f6e64 100644 --- a/protocol-designer/src/step-forms/utils/index.ts +++ b/protocol-designer/src/step-forms/utils/index.ts @@ -123,23 +123,24 @@ export const getSlotIsEmpty = ( } const filteredAdditionalEquipmentOnDeck = includeStagingAreas - ? values(initialDeckSetup.additionalEquipmentOnDeck).filter( - (additionalEquipment: AdditionalEquipmentOnDeck) => - additionalEquipment.location === slot + ? values( + initialDeckSetup.additionalEquipmentOnDeck + ).filter((additionalEquipment: AdditionalEquipmentOnDeck) => + additionalEquipment.location?.includes(slot) ) : values(initialDeckSetup.additionalEquipmentOnDeck).filter( (additionalEquipment: AdditionalEquipmentOnDeck) => - additionalEquipment.location === slot && + additionalEquipment.location?.includes(slot) && additionalEquipment.name !== 'stagingArea' ) return ( [ - ...values(initialDeckSetup.modules).filter( - (moduleOnDeck: ModuleOnDeck) => moduleOnDeck.slot === slot + ...values(initialDeckSetup.modules).filter((moduleOnDeck: ModuleOnDeck) => + slot.includes(moduleOnDeck.slot) ), - ...values(initialDeckSetup.labware).filter( - (labware: LabwareOnDeckType) => labware.slot === slot + ...values(initialDeckSetup.labware).filter((labware: LabwareOnDeckType) => + slot.includes(labware.slot) ), ...filteredAdditionalEquipmentOnDeck, ].length === 0 diff --git a/protocol-designer/src/steplist/fieldLevel/index.ts b/protocol-designer/src/steplist/fieldLevel/index.ts index 0eae673ff73..d9a86262b12 100644 --- a/protocol-designer/src/steplist/fieldLevel/index.ts +++ b/protocol-designer/src/steplist/fieldLevel/index.ts @@ -42,6 +42,7 @@ import { PipetteEntity, InvariantContext, LabwareEntities, + AdditionalEquipmentEntities, } from '@opentrons/step-generation' import { StepFieldName } from '../../form-types' import type { LabwareLocation } from '@opentrons/shared-data' @@ -64,6 +65,14 @@ const getIsAdapterLocation = ( labwareEntities[newLocation].def.allowedRoles?.includes('adapter') ?? false ) } +const getIsWasteChuteLocation = ( + newLocation: string, + additionalEquipmentEntities: AdditionalEquipmentEntities +): boolean => + Object.values(additionalEquipmentEntities).find( + aE => aE.location === newLocation && aE.name === 'wasteChute' + ) != null + const getLabwareLocation = ( state: InvariantContext, newLocationString: string @@ -77,6 +86,15 @@ const getLabwareLocation = ( getIsAdapterLocation(newLocationString, state.labwareEntities) ) { return { labwareId: newLocationString } + } else if ( + getIsWasteChuteLocation( + newLocationString, + state.additionalEquipmentEntities + ) + ) { + return { + addressableAreaName: 'gripperWasteChute', + } } else { return { slotName: newLocationString } } diff --git a/protocol-designer/src/timelineMiddleware/generateRobotStateTimeline.ts b/protocol-designer/src/timelineMiddleware/generateRobotStateTimeline.ts index 72ff5301b8e..544a14dca10 100644 --- a/protocol-designer/src/timelineMiddleware/generateRobotStateTimeline.ts +++ b/protocol-designer/src/timelineMiddleware/generateRobotStateTimeline.ts @@ -2,6 +2,7 @@ import takeWhile from 'lodash/takeWhile' import * as StepGeneration from '@opentrons/step-generation' import { commandCreatorFromStepArgs } from '../file-data/selectors/commands' import type { StepArgsAndErrorsById } from '../steplist/types' + export interface GenerateRobotStateTimelineArgs { allStepArgsAndErrors: StepArgsAndErrorsById orderedStepIds: string[] @@ -65,18 +66,36 @@ export const generateRobotStateTimeline = ( // @ts-expect-error(sa, 2021-6-20): not a valid type narrow, use in operator nextStepArgsForPipette.changeTip === 'never' + const isWasteChute = + invariantContext.additionalEquipmentEntities[dropTipLocation] != null + + const pipetteSpec = invariantContext.pipetteEntities[pipetteId]?.spec + + const addressableAreaName = + pipetteSpec.channels === 96 + ? '96ChannelWasteChute' + : '1and8ChannelWasteChute' + + const dropTipCommand = isWasteChute + ? StepGeneration.curryCommandCreator( + StepGeneration.wasteChuteCommandsUtil, + { + type: 'dropTip', + pipetteId: pipetteId, + addressableAreaName, + } + ) + : StepGeneration.curryCommandCreator(StepGeneration.dropTip, { + pipette: pipetteId, + dropTipLocation, + }) + if (!willReuseTip) { return [ ...acc, (_invariantContext, _prevRobotState) => StepGeneration.reduceCommandCreators( - [ - curriedCommandCreator, - StepGeneration.curryCommandCreator(StepGeneration.dropTip, { - pipette: pipetteId, - dropTipLocation, - }), - ], + [curriedCommandCreator, dropTipCommand], _invariantContext, _prevRobotState ), diff --git a/protocol-designer/src/top-selectors/labware-locations/index.ts b/protocol-designer/src/top-selectors/labware-locations/index.ts index 5db5f8f7509..94384f44c7f 100644 --- a/protocol-designer/src/top-selectors/labware-locations/index.ts +++ b/protocol-designer/src/top-selectors/labware-locations/index.ts @@ -5,8 +5,14 @@ import { getDeckDefFromRobotType, getModuleDisplayName, FLEX_ROBOT_TYPE, - WASTE_CHUTE_SLOT, + WASTE_CHUTE_ADDRESSABLE_AREAS, + WASTE_CHUTE_CUTOUT, + CutoutId, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + isAddressableAreaStandardSlot, + MOVABLE_TRASH_ADDRESSABLE_AREAS, } from '@opentrons/shared-data' +import { COLUMN_4_SLOTS } from '@opentrons/step-generation' import { START_TERMINAL_ITEM_ID, END_TERMINAL_ITEM_ID, @@ -107,9 +113,13 @@ export const getUnocuppiedLabwareLocationOptions: Selector< additionalEquipmentEntities ) => { const deckDef = getDeckDefFromRobotType(robotType) - const trashSlot = robotType === FLEX_ROBOT_TYPE ? 'A3' : '12' - const allSlotIds = deckDef.locations.orderedSlots.map(slot => slot.id) + const cutoutFixtures = deckDef.cutoutFixtures + const allSlotIds = deckDef.locations.addressableAreas.map(slot => slot.id) const hasWasteChute = getHasWasteChute(additionalEquipmentEntities) + const stagingAreaCutoutIds = Object.values(additionalEquipmentEntities) + .filter(aE => aE.name === 'stagingArea') + // TODO(jr, 11/13/23): fix AdditionalEquipment['location'] from type string to CutoutId + .map(aE => aE.location as CutoutId) if (robotState == null) return null @@ -132,7 +142,6 @@ export const getUnocuppiedLabwareLocationOptions: Selector< const labwareOnAdapter = Object.values(labware).find( temporalProperties => temporalProperties.slot === labwareId ) - const adapterSlot = labwareOnDeck.slot const modIdWithAdapter = Object.keys(modules).find( modId => modId === labwareOnDeck.slot @@ -189,21 +198,44 @@ export const getUnocuppiedLabwareLocationOptions: Selector< [] ) + const stagingAreaAddressableAreaNames = stagingAreaCutoutIds + .flatMap(cutoutId => { + const addressableAreasOnCutout = cutoutFixtures.find( + cutoutFixture => cutoutFixture.id === STAGING_AREA_RIGHT_SLOT_FIXTURE + )?.providesAddressableAreas[cutoutId] + return addressableAreasOnCutout ?? [] + }) + .filter(aa => !isAddressableAreaStandardSlot(aa, deckDef)) + + // TODO(jr, 11/13/23): update COLUMN_4_SLOTS usage to FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS + const notSelectedStagingAreaAddressableAreas = COLUMN_4_SLOTS.filter(slot => + stagingAreaAddressableAreaNames.every( + addressableArea => addressableArea !== slot + ) + ) + const unoccupiedSlotOptions = allSlotIds - .filter( - slotId => + .filter(slotId => { + const isTrashSlot = + robotType === FLEX_ROBOT_TYPE + ? MOVABLE_TRASH_ADDRESSABLE_AREAS.includes(slotId) + : slotId === 'fixedTrash' + + return ( !slotIdsOccupiedByModules.includes(slotId) && !Object.values(labware) .map(lw => lw.slot) .includes(slotId) && - slotId !== trashSlot && - (hasWasteChute ? slotId !== WASTE_CHUTE_SLOT : true) - ) + !isTrashSlot && + !WASTE_CHUTE_ADDRESSABLE_AREAS.includes(slotId) && + !notSelectedStagingAreaAddressableAreas.includes(slotId) + ) + }) .map(slotId => ({ name: slotId, value: slotId })) const offDeck = { name: 'Off-deck', value: 'offDeck' } const wasteChuteSlot = { name: 'Waste Chute in D3', - value: WASTE_CHUTE_SLOT, + value: WASTE_CHUTE_CUTOUT, } return hasWasteChute diff --git a/protocol-designer/src/tutorial/index.ts b/protocol-designer/src/tutorial/index.ts index e5a1a71d97a..a0eee9ffff3 100644 --- a/protocol-designer/src/tutorial/index.ts +++ b/protocol-designer/src/tutorial/index.ts @@ -10,7 +10,7 @@ type HintKey = // normal hints | 'waste_chute_warning' // blocking hints | 'custom_labware_with_modules' - | 'export_v7_protocol_7_0' + | 'export_v8_protocol_7_1' | 'change_magnet_module_model' // DEPRECATED HINTS (keep a record to avoid name collisions with old persisted dismissal states) // 'export_v4_protocol' @@ -18,5 +18,6 @@ type HintKey = // normal hints // | 'export_v5_protocol_3_20' // | 'export_v6_protocol_6_10' // | 'export_v6_protocol_6_20' +// | 'export_v7_protocol_7_0' export { actions, rootReducer, selectors } export type { RootState, HintKey } diff --git a/protocol-designer/src/tutorial/selectors.ts b/protocol-designer/src/tutorial/selectors.ts index ec198989d88..a1185dbee77 100644 --- a/protocol-designer/src/tutorial/selectors.ts +++ b/protocol-designer/src/tutorial/selectors.ts @@ -1,7 +1,7 @@ import { createSelector } from 'reselect' import { THERMOCYCLER_MODULE_TYPE, - WASTE_CHUTE_SLOT, + WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import { timelineFrameBeforeActiveItem } from '../top-selectors/timelineFrames' import { @@ -85,7 +85,7 @@ export const shouldShowWasteChuteHint: Selector = createSelector( return false } const { newLocation } = unsavedForm - if (newLocation === WASTE_CHUTE_SLOT) { + if (newLocation === WASTE_CHUTE_CUTOUT) { return true } diff --git a/protocol-designer/src/ui/labware/__tests__/selectors.test.ts b/protocol-designer/src/ui/labware/__tests__/selectors.test.ts index c3388d91825..2e3ba647add 100644 --- a/protocol-designer/src/ui/labware/__tests__/selectors.test.ts +++ b/protocol-designer/src/ui/labware/__tests__/selectors.test.ts @@ -149,6 +149,7 @@ describe('labware selectors', () => { names, initialDeckSetup, {}, + {}, {} ) ).toEqual([ @@ -179,6 +180,7 @@ describe('labware selectors', () => { names, initialDeckSetup, presavedStepForm, + {}, {} ) ).toEqual([ @@ -261,6 +263,7 @@ describe('labware selectors', () => { nicknames, initialDeckSetup, {}, + {}, {} ) ).toEqual([ @@ -317,11 +320,13 @@ describe('labware selectors', () => { labwareEntities, nicknames, initialDeckSetup, - savedStep + {}, + savedStep, + {} ) ).toEqual([ { name: 'Trash', value: mockTrash }, - { name: 'Well Plate in Magnetic Module', value: 'wellPlateId' }, + { name: 'Well Plate', value: 'wellPlateId' }, ]) }) }) diff --git a/protocol-designer/src/ui/labware/selectors.ts b/protocol-designer/src/ui/labware/selectors.ts index 7ee927c1ff7..128d5831580 100644 --- a/protocol-designer/src/ui/labware/selectors.ts +++ b/protocol-designer/src/ui/labware/selectors.ts @@ -6,6 +6,7 @@ import { getLabwareDisplayName, getLabwareHasQuirk, } from '@opentrons/shared-data' +import { COLUMN_4_SLOTS } from '@opentrons/step-generation' import { i18n } from '../../localization' import * as stepFormSelectors from '../../step-forms/selectors' import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' @@ -48,14 +49,20 @@ export const getLabwareOptions: Selector = createSelector( stepFormSelectors.getInitialDeckSetup, stepFormSelectors.getPresavedStepForm, stepFormSelectors.getSavedStepForms, + stepFormSelectors.getAdditionalEquipmentEntities, ( labwareEntities, nicknamesById, initialDeckSetup, presavedStepForm, - savedStepForms + savedStepForms, + additionalEquipmentEntities ) => { const moveLabwarePresavedStep = presavedStepForm?.stepType === 'moveLabware' + const wasteChuteLocation = Object.values(additionalEquipmentEntities).find( + aE => aE.name === 'wasteChute' + )?.location + const options = reduce( labwareEntities, ( @@ -63,15 +70,41 @@ export const getLabwareOptions: Selector = createSelector( labwareEntity: LabwareEntity, labwareId: string ): Options => { + const isLabwareInWasteChute = Object.values(savedStepForms).find( + form => + form.stepType === 'moveLabware' && + form.labware === labwareId && + form.newLocation === wasteChuteLocation + ) + const isAdapter = labwareEntity.def.allowedRoles?.includes('adapter') const isOffDeck = getLabwareOffDeck( initialDeckSetup, savedStepForms ?? {}, labwareId ) + const isStartingInColumn4 = COLUMN_4_SLOTS.includes( + initialDeckSetup.labware[labwareId]?.slot + ) + + const isInColumn4 = + savedStepForms != null + ? Object.values(savedStepForms) + ?.reverse() + .some( + form => + form.stepType === 'moveLabware' && + form.labware === labwareId && + (COLUMN_4_SLOTS.includes(form.newLocation) || + (isStartingInColumn4 && + !COLUMN_4_SLOTS.includes(form.newLocation))) + ) + : false + const isAdapterOrAluminumBlock = isAdapter || labwareEntity.def.metadata.displayCategory === 'aluminumBlock' + const moduleOnDeck = getModuleUnderLabware( initialDeckSetup, savedStepForms ?? {}, @@ -91,10 +124,16 @@ export const getLabwareOptions: Selector = createSelector( nickName = `Off-deck - ${nicknamesById[labwareId]}` } else if (nickName === 'Opentrons Fixed Trash') { nickName = TRASH + } else if (isInColumn4) { + nickName = `${nicknamesById[labwareId]} in staging area slot` } if (!moveLabwarePresavedStep) { - return getIsTiprack(labwareEntity.def) || isAdapter + // filter out tip racks, adapters, and labware in waste chute + // for aspirating/dispensing/mixing into + return getIsTiprack(labwareEntity.def) || + isAdapter || + isLabwareInWasteChute ? acc : [ ...acc, @@ -104,8 +143,11 @@ export const getLabwareOptions: Selector = createSelector( }, ] } else { - // filter out moving trash for now in MoveLabware step type - return nickName === TRASH || isAdapterOrAluminumBlock + // filter out moving trash, aluminum blocks, adapters and labware in + // waste chute for moveLabware + return nickName === TRASH || + isAdapterOrAluminumBlock || + isLabwareInWasteChute ? acc : [ ...acc, diff --git a/protocol-designer/src/utils/index.ts b/protocol-designer/src/utils/index.ts index 210016ffa45..0da2da0b603 100644 --- a/protocol-designer/src/utils/index.ts +++ b/protocol-designer/src/utils/index.ts @@ -1,5 +1,14 @@ import uuidv1 from 'uuid/v4' -import { WellSetHelpers, makeWellSetHelpers } from '@opentrons/shared-data' +import { + WellSetHelpers, + makeWellSetHelpers, + AddressableAreaName, + getDeckDefFromRobotType, + FLEX_ROBOT_TYPE, + CutoutId, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + isAddressableAreaStandardSlot, +} from '@opentrons/shared-data' import { i18n } from '../localization' import { WellGroup } from '@opentrons/components' import { BoundingRect, GenericRect } from '../collision-types' @@ -132,3 +141,19 @@ export const getStagingAreaSlots = ( export const getHas96Channel = (pipettes: PipetteEntities): boolean => { return Object.values(pipettes).some(pip => pip.spec.channels === 96) } + +export const getStagingAreaAddressableAreas = ( + cutoutIds: CutoutId[] +): AddressableAreaName[] => { + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) + const cutoutFixtures = deckDef.cutoutFixtures + + return cutoutIds + .flatMap(cutoutId => { + const addressableAreasOnCutout = cutoutFixtures.find( + cutoutFixture => cutoutFixture.id === STAGING_AREA_RIGHT_SLOT_FIXTURE + )?.providesAddressableAreas[cutoutId] + return addressableAreasOnCutout ?? [] + }) + .filter(aa => !isAddressableAreaStandardSlot(aa, deckDef)) +} diff --git a/react-api-client/src/deck_configuration/__tests__/useCreateDeckConfigurationMutation.test.tsx b/react-api-client/src/deck_configuration/__tests__/useCreateDeckConfigurationMutation.test.tsx deleted file mode 100644 index d2e201fc0e0..00000000000 --- a/react-api-client/src/deck_configuration/__tests__/useCreateDeckConfigurationMutation.test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import * as React from 'react' -import { when, resetAllWhenMocks } from 'jest-when' -import { QueryClient, QueryClientProvider } from 'react-query' -import { renderHook } from '@testing-library/react-hooks' - -import { createDeckConfiguration } from '@opentrons/api-client' -// import { -// TRASH_BIN_LOAD_NAME, -// WASTE_CHUTE_LOAD_NAME, -// WASTE_CHUTE_SLOT, -// } from '@opentrons/shared-data' - -import { useHost } from '../../api' -import { useCreateDeckConfigurationMutation } from '..' - -import type { HostConfig } from '@opentrons/api-client' -// import type { DeckConfiguration } from '@opentrons/shared-data' - -jest.mock('@opentrons/api-client') -jest.mock('../../api/useHost') - -const mockCreateDeckConfiguration = createDeckConfiguration as jest.MockedFunction< - typeof createDeckConfiguration -> -const mockUseHost = useHost as jest.MockedFunction - -// const mockDeckConfiguration = [ -// { -// fixtureId: 'mockFixtureWasteChuteId', -// fixtureLocation: 'D3', -// loadName: WASTE_CHUTE_LOAD_NAME, -// }, -// ] as DeckConfiguration - -const HOST_CONFIG: HostConfig = { hostname: 'localhost' } - -describe('useCreateDeckConfigurationMutation hook', () => { - let wrapper: React.FunctionComponent<{}> - - beforeEach(() => { - const queryClient = new QueryClient() - const clientProvider: React.FunctionComponent<{}> = ({ children }) => ( - {children} - ) - - wrapper = clientProvider - }) - - afterEach(() => { - resetAllWhenMocks() - }) - - it('should return no data when calling createDeckConfiguration if the request fails', async () => { - when(mockUseHost).calledWith().mockReturnValue(HOST_CONFIG) - when(mockCreateDeckConfiguration) - .calledWith(HOST_CONFIG, []) - .mockRejectedValue('oh no') - - const { result, waitFor } = renderHook( - () => useCreateDeckConfigurationMutation(), - { - wrapper, - } - ) - expect(result.current.data).toBeUndefined() - result.current.createDeckConfiguration([]) - await waitFor(() => { - return result.current.status !== 'loading' - }) - expect(result.current.data).toBeUndefined() - }) - // ToDo (kk:10/25/2023) this part will be update when backend is ready - // it('should create a run when calling createDeckConfiguration callback with DeckConfiguration', async () => { - // when(useHost).calledWith().mockReturnValue(HOST_CONFIG) - // when(mockCreateDeckConfiguration) - // .calledWith(HOST_CONFIG, mockDeckConfiguration) - // .mockResolvedValue({ - // data: mockCreateDeckConfiguration, - // } as Response) - - // const { result, waitFor } = renderHook(useCreateDeckConfigurationMutation, { - // wrapper, - // }) - // act(() => result.current.createDeckConfiguration(mockDeckConfiguration)) - - // await waitFor(() => result.current.data != null) - // expect(result.current.data).toEqual(mockDeckConfiguration) - // }) -}) diff --git a/react-api-client/src/deck_configuration/index.ts b/react-api-client/src/deck_configuration/index.ts index b6237d14c30..063a5b0fe82 100644 --- a/react-api-client/src/deck_configuration/index.ts +++ b/react-api-client/src/deck_configuration/index.ts @@ -1,3 +1,2 @@ -export { useCreateDeckConfigurationMutation } from './useCreateDeckConfigurationMutation' export { useDeckConfigurationQuery } from './useDeckConfigurationQuery' export { useUpdateDeckConfigurationMutation } from './useUpdateDeckConfigurationMutation' diff --git a/react-api-client/src/deck_configuration/useCreateDeckConfigurationMutation.ts b/react-api-client/src/deck_configuration/useCreateDeckConfigurationMutation.ts deleted file mode 100644 index ad898e8c13b..00000000000 --- a/react-api-client/src/deck_configuration/useCreateDeckConfigurationMutation.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useMutation, useQueryClient } from 'react-query' -import { createDeckConfiguration } from '@opentrons/api-client' -import { useHost } from '../api' - -import type { - UseMutateFunction, - UseMutationOptions, - UseMutationResult, -} from 'react-query' -import type { AxiosError } from 'axios' -import type { DeckConfiguration } from '@opentrons/shared-data' -import type { ErrorResponse, HostConfig } from '@opentrons/api-client' - -const DECK_CONFIGURATION = 'deck_configuration' - -export type UseCreateDeckConfigurationMutationResult = UseMutationResult< - DeckConfiguration, - AxiosError, - DeckConfiguration -> & { - createDeckConfiguration: UseMutateFunction< - DeckConfiguration, - AxiosError, - DeckConfiguration - > -} - -export type UseCreateDeckConfigurationMutationOptions = UseMutationOptions< - DeckConfiguration, - AxiosError, - DeckConfiguration -> - -export function useCreateDeckConfigurationMutation( - options: UseCreateDeckConfigurationMutationOptions = {} -): UseCreateDeckConfigurationMutationResult { - const host = useHost() - const queryClient = useQueryClient() - - const mutation = useMutation< - DeckConfiguration, - AxiosError, - DeckConfiguration - >( - [host, DECK_CONFIGURATION], - (deckConfiguration: DeckConfiguration) => - createDeckConfiguration(host as HostConfig, deckConfiguration).then( - response => { - queryClient - .invalidateQueries([host, DECK_CONFIGURATION]) - .catch((error: Error) => { - throw error - }) - return response.data - } - ), - options - ) - return { - ...mutation, - createDeckConfiguration: mutation.mutate, - } -} diff --git a/react-api-client/src/deck_configuration/useDeckConfigurationQuery.ts b/react-api-client/src/deck_configuration/useDeckConfigurationQuery.ts index babee32c609..2851665ff8a 100644 --- a/react-api-client/src/deck_configuration/useDeckConfigurationQuery.ts +++ b/react-api-client/src/deck_configuration/useDeckConfigurationQuery.ts @@ -12,7 +12,9 @@ export function useDeckConfigurationQuery( const query = useQuery( [host, 'deck_configuration'], () => - getDeckConfiguration(host as HostConfig).then(response => response.data), + getDeckConfiguration(host as HostConfig).then( + response => response.data?.data?.cutoutFixtures ?? [] + ), { enabled: host !== null, ...options } ) diff --git a/react-api-client/src/deck_configuration/useUpdateDeckConfigurationMutation.ts b/react-api-client/src/deck_configuration/useUpdateDeckConfigurationMutation.ts index 13dc7e0a155..8d62801619b 100644 --- a/react-api-client/src/deck_configuration/useUpdateDeckConfigurationMutation.ts +++ b/react-api-client/src/deck_configuration/useUpdateDeckConfigurationMutation.ts @@ -12,24 +12,24 @@ import { useHost } from '../api' import type { AxiosError } from 'axios' import type { ErrorResponse, HostConfig } from '@opentrons/api-client' -import type { Fixture } from '@opentrons/shared-data' +import type { DeckConfiguration } from '@opentrons/shared-data' export type UseUpdateDeckConfigurationMutationResult = UseMutationResult< - Omit, + DeckConfiguration, AxiosError, - Omit + DeckConfiguration > & { updateDeckConfiguration: UseMutateFunction< - Omit, + DeckConfiguration, AxiosError, - Omit + DeckConfiguration > } export type UseUpdateDeckConfigurationMutationOptions = UseMutationOptions< - Omit, + DeckConfiguration, AxiosError, - Omit + DeckConfiguration > export function useUpdateDeckConfigurationMutation( @@ -39,19 +39,19 @@ export function useUpdateDeckConfigurationMutation( const queryClient = useQueryClient() const mutation = useMutation< - Omit, + DeckConfiguration, AxiosError, - Omit + DeckConfiguration >( [host, 'deck_configuration'], - (fixture: Omit) => - updateDeckConfiguration(host as HostConfig, fixture).then(response => { + (deckConfig: DeckConfiguration) => + updateDeckConfiguration(host as HostConfig, deckConfig).then(response => { queryClient .invalidateQueries([host, 'deck_configuration']) .catch((e: Error) => { throw e }) - return response.data + return response.data?.data?.cutoutFixtures ?? [] }), options ) diff --git a/react-api-client/src/runs/useAllRunsQuery.ts b/react-api-client/src/runs/useAllRunsQuery.ts index a1e3665860f..35bc910c67b 100644 --- a/react-api-client/src/runs/useAllRunsQuery.ts +++ b/react-api-client/src/runs/useAllRunsQuery.ts @@ -3,10 +3,11 @@ import { useQuery } from 'react-query' import { useHost } from '../api' import type { UseQueryOptions, UseQueryResult } from 'react-query' +import type { AxiosError } from 'axios' export type UseAllRunsQueryOptions = UseQueryOptions< Runs, - Error, + AxiosError, Runs, Array > @@ -15,7 +16,7 @@ export function useAllRunsQuery( params: GetRunsParams = {}, options: UseAllRunsQueryOptions = {}, hostOverride?: HostConfig | null -): UseQueryResult { +): UseQueryResult { const contextHost = useHost() const host = hostOverride != null ? { ...contextHost, ...hostOverride } : contextHost @@ -25,7 +26,12 @@ export function useAllRunsQuery( } const query = useQuery( queryKey, - () => getRuns(host as HostConfig, params).then(response => response.data), + () => + getRuns(host as HostConfig, params) + .then(response => response.data) + .catch((e: AxiosError) => { + throw e + }), { enabled: host !== null, ...options } ) diff --git a/robot-server/robot_server/deck_configuration/__init__.py b/robot-server/robot_server/deck_configuration/__init__.py new file mode 100644 index 00000000000..13bacaf8a4d --- /dev/null +++ b/robot-server/robot_server/deck_configuration/__init__.py @@ -0,0 +1 @@ +"""The HTTP API, and supporting code, for getting and setting the robot's deck configuration.""" diff --git a/robot-server/robot_server/deck_configuration/defaults.py b/robot-server/robot_server/deck_configuration/defaults.py new file mode 100644 index 00000000000..a591e9798df --- /dev/null +++ b/robot-server/robot_server/deck_configuration/defaults.py @@ -0,0 +1,115 @@ +"""Default deck configurations.""" + + +from . import models + + +_for_flex = models.DeckConfigurationRequest.construct( + cutoutFixtures=[ + models.CutoutFixture.construct( + cutoutId="cutoutA1", cutoutFixtureId="singleLeftSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutoutB1", cutoutFixtureId="singleLeftSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutoutC1", cutoutFixtureId="singleLeftSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutoutD1", cutoutFixtureId="singleLeftSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutoutA2", cutoutFixtureId="singleCenterSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutoutB2", cutoutFixtureId="singleCenterSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutoutC2", cutoutFixtureId="singleCenterSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutoutD2", cutoutFixtureId="singleCenterSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutoutA3", cutoutFixtureId="trashBinAdapter" + ), + models.CutoutFixture.construct( + cutoutId="cutoutB3", cutoutFixtureId="singleRightSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutoutC3", cutoutFixtureId="singleRightSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutoutD3", cutoutFixtureId="singleRightSlot" + ), + ] +) + + +_for_ot2 = models.DeckConfigurationRequest.construct( + cutoutFixtures=[ + models.CutoutFixture.construct( + cutoutId="cutout1", cutoutFixtureId="singleStandardSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutout2", cutoutFixtureId="singleStandardSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutout3", cutoutFixtureId="singleStandardSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutout4", cutoutFixtureId="singleStandardSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutout5", cutoutFixtureId="singleStandardSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutout6", cutoutFixtureId="singleStandardSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutout7", cutoutFixtureId="singleStandardSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutout8", cutoutFixtureId="singleStandardSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutout9", cutoutFixtureId="singleStandardSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutout10", cutoutFixtureId="singleStandardSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutout11", cutoutFixtureId="singleStandardSlot" + ), + models.CutoutFixture.construct( + cutoutId="cutout12", cutoutFixtureId="fixedTrashSlot" + ), + ] +) + + +def for_deck_definition(deck_definition_name: str) -> models.DeckConfigurationRequest: + """Return a default configuration for the given deck definition. + + When a user has not yet configured which fixtures are on a robot, the robot should fall back to + this for the purposes of running protocols. + + Protocol analysis should *not* use this default. Analysis is supposed to *determine* the + protocol's deck configuration requirements, instead of assuming some default. + + Params: + deck_definition_name: The name of a deck definition loadable through + `opentrons_shared_data.deck`. + """ + try: + return { + "ot2_standard": _for_ot2, + "ot2_short_trash": _for_ot2, + "ot3_standard": _for_flex, + }[deck_definition_name] + except KeyError as exception: + # This shouldn't happen. Every deck definition that a robot might have should have a + # default configuration. + raise ValueError( + f"The deck {deck_definition_name} has no default configuration." + ) from exception diff --git a/robot-server/robot_server/deck_configuration/fastapi_dependencies.py b/robot-server/robot_server/deck_configuration/fastapi_dependencies.py new file mode 100644 index 00000000000..4fe27f77275 --- /dev/null +++ b/robot-server/robot_server/deck_configuration/fastapi_dependencies.py @@ -0,0 +1,31 @@ +"""Dependency functions for use with `fastapi.Depends()`.""" + + +import fastapi + +from opentrons.protocol_engine.types import DeckType +from server_utils.fastapi_utils.app_state import ( + AppState, + AppStateAccessor, + get_app_state, +) + +from robot_server.deck_configuration.store import DeckConfigurationStore +from robot_server.hardware import get_deck_type + + +_accessor = AppStateAccessor[DeckConfigurationStore]("deck_configuration_store") + + +async def get_deck_configuration_store( + app_state: AppState = fastapi.Depends(get_app_state), + deck_type: DeckType = fastapi.Depends(get_deck_type), +) -> DeckConfigurationStore: + """Return the server's singleton `DeckConfigurationStore`.""" + deck_configuration_store = _accessor.get_from(app_state) + if deck_configuration_store is None: + # If this initialization becomes async, we will need to protect it with a lock, + # to protect against the bug described in https://github.com/Opentrons/opentrons/pull/11927. + deck_configuration_store = DeckConfigurationStore(deck_type) + _accessor.set_on(app_state, deck_configuration_store) + return deck_configuration_store diff --git a/robot-server/robot_server/deck_configuration/models.py b/robot-server/robot_server/deck_configuration/models.py new file mode 100644 index 00000000000..a0f2b6b7114 --- /dev/null +++ b/robot-server/robot_server/deck_configuration/models.py @@ -0,0 +1,64 @@ +"""HTTP-facing JSON models for deck configuration.""" + + +from datetime import datetime +from typing import List, Optional +from typing_extensions import Literal + +import pydantic + +from robot_server.errors import ErrorDetails + + +class CutoutFixture(pydantic.BaseModel): + """A single element of the robot's deck configuration.""" + + # These are deliberately typed as plain strs, instead of Enums or Literals, + # because we want shared-data to be the source of truth. + # + # The downside of this is that to use this HTTP interface, you need to be familiar with deck + # definitions. To make this better, we could perhaps autogenerate OpenAPI / JSON Schema spec + # fragments from shared-data and inject them here. + cutoutFixtureId: str = pydantic.Field( + description=( + "What kind of cutout fixture is mounted onto the deck." + " Valid values are the `id`s of `cutoutFixtures` in the" + " [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck)." + ) + ) + cutoutId: str = pydantic.Field( + description=( + "Where on the deck this cutout fixture is mounted." + " Valid values are the `id`s of `cutouts` in the" + " [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck)." + ) + ) + + +class DeckConfigurationRequest(pydantic.BaseModel): + """A request to set the robot's deck configuration.""" + + cutoutFixtures: List[CutoutFixture] = pydantic.Field( + description="A full list of all the cutout fixtures that are mounted onto the deck." + ) + + +class DeckConfigurationResponse(pydantic.BaseModel): + """A response for the robot's current deck configuration.""" + + cutoutFixtures: List[CutoutFixture] = pydantic.Field( + description="A full list of all the cutout fixtures that are mounted onto the deck." + ) + lastUpdatedAt: Optional[datetime] = pydantic.Field( + description=( + "When the deck configuration was last set over HTTP." + " If that has never happened, this will be `null` or omitted." + ) + ) + + +class InvalidDeckConfiguration(ErrorDetails): + """Error details for when a client supplies an invalid deck configuration.""" + + id: Literal["InvalidDeckConfiguration"] = "InvalidDeckConfiguration" + title: str = "Invalid Deck Configuration" diff --git a/robot-server/robot_server/deck_configuration/router.py b/robot-server/robot_server/deck_configuration/router.py new file mode 100644 index 00000000000..af0b7e80498 --- /dev/null +++ b/robot-server/robot_server/deck_configuration/router.py @@ -0,0 +1,103 @@ +"""The HTTP API for getting and setting the robot's current deck configuration.""" + + +from datetime import datetime +from typing import Union + +import fastapi +from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY + +from opentrons_shared_data.deck.dev_types import DeckDefinitionV4 + +from robot_server.errors import ErrorBody +from robot_server.hardware import get_deck_definition +from robot_server.service.dependencies import get_current_time +from robot_server.service.json_api import PydanticResponse, RequestModel, SimpleBody + +from . import models +from . import validation +from . import validation_mapping +from .fastapi_dependencies import get_deck_configuration_store +from .store import DeckConfigurationStore + + +router = fastapi.APIRouter() + + +@router.put( + path="/deck_configuration", + summary="Set the deck configuration", + description=( + "Inform the robot how its deck is physically set up." + "\n\n" + "When you use the `/runs` and `/maintenance_runs` endpoints to command the robot to move," + " the robot will automatically dodge the obstacles that you declare here." + "\n\n" + "If a run command tries to do something that inherently conflicts with this deck" + " configuration, such as loading a labware into a staging area slot that this deck" + " configuration doesn't provide, the run command will fail with an error." + "\n\n" + "**Warning:**" + " Currently, you can call this endpoint at any time, even while there is an active run." + " However, the robot can't adapt to deck configuration changes in the middle of a run." + " The robot will effectively take a snapshot of the deck configuration when the run is" + " first played. In the future, this endpoint may error if you try to call it in the middle" + " of an active run, so don't rely on being able to do that." + "\n\n" + "After you set the deck configuration, it will persist, even across reboots," + " until you set it to something else." + ), + responses={ + fastapi.status.HTTP_200_OK: { + "model": SimpleBody[models.DeckConfigurationResponse] + }, + fastapi.status.HTTP_422_UNPROCESSABLE_ENTITY: { + "model": ErrorBody[models.InvalidDeckConfiguration] + }, + }, +) +async def put_deck_configuration( # noqa: D103 + request_body: RequestModel[models.DeckConfigurationRequest], + store: DeckConfigurationStore = fastapi.Depends(get_deck_configuration_store), + last_updated_at: datetime = fastapi.Depends(get_current_time), + deck_definition: DeckDefinitionV4 = fastapi.Depends(get_deck_definition), +) -> PydanticResponse[ + Union[ + SimpleBody[models.DeckConfigurationResponse], + ErrorBody[models.InvalidDeckConfiguration], + ] +]: + placements = validation_mapping.map_in(request_body.data) + validation_errors = validation.get_configuration_errors(deck_definition, placements) + if len(validation_errors) == 0: + success_data = await store.set(request_body.data, last_updated_at) + return await PydanticResponse.create( + content=SimpleBody.construct(data=success_data) + ) + else: + error_data = validation_mapping.map_out(validation_errors) + return await PydanticResponse.create( + content=ErrorBody.construct(errors=error_data), + status_code=HTTP_422_UNPROCESSABLE_ENTITY, + ) + + +@router.get( + "/deck_configuration", + summary="Get the deck configuration", + description=( + "Get the robot's current deck configuration." + " See `PUT /deck_configuration` for background information." + ), + responses={ + fastapi.status.HTTP_200_OK: { + "model": SimpleBody[models.DeckConfigurationResponse] + }, + }, +) +async def get_deck_configuration( # noqa: D103 + store: DeckConfigurationStore = fastapi.Depends(get_deck_configuration_store), +) -> PydanticResponse[SimpleBody[models.DeckConfigurationResponse]]: + return await PydanticResponse.create( + content=SimpleBody.construct(data=await store.get()) + ) diff --git a/robot-server/robot_server/deck_configuration/store.py b/robot-server/robot_server/deck_configuration/store.py new file mode 100644 index 00000000000..51f9f9c5281 --- /dev/null +++ b/robot-server/robot_server/deck_configuration/store.py @@ -0,0 +1,58 @@ +# noqa: D100 + +from dataclasses import dataclass +from datetime import datetime +from typing import List, Optional + +from opentrons.protocol_engine.types import DeckType + +from . import defaults +from . import models +from opentrons.protocol_engine.types import DeckConfigurationType + + +# TODO(mm, 2023-11-17): Add unit tests for DeckConfigurationStore. +class DeckConfigurationStore: + """An in-memory stand-in for a persistent store of the robot's deck configuration.""" + + def __init__(self, deck_type: DeckType) -> None: + self._deck_type = deck_type + self._last_update: Optional[_LastUpdate] = None + + async def set( + self, request: models.DeckConfigurationRequest, last_updated_at: datetime + ) -> models.DeckConfigurationResponse: + """Set the robot's current deck configuration.""" + self._last_update = _LastUpdate( + cutout_fixtures=request.cutoutFixtures, timestamp=last_updated_at + ) + return await self.get() + + async def get(self) -> models.DeckConfigurationResponse: + """Get the robot's current deck configuration.""" + if self._last_update is None: + return models.DeckConfigurationResponse.construct( + cutoutFixtures=defaults.for_deck_definition( + self._deck_type.value + ).cutoutFixtures, + lastUpdatedAt=None, + ) + else: + return models.DeckConfigurationResponse.construct( + cutoutFixtures=self._last_update.cutout_fixtures, + lastUpdatedAt=self._last_update.timestamp, + ) + + async def get_deck_configuration(self) -> DeckConfigurationType: + """Get the robot's current deck configuration in an expected typing.""" + to_convert = await self.get() + converted = [ + (item.cutoutId, item.cutoutFixtureId) for item in to_convert.cutoutFixtures + ] + return converted + + +@dataclass +class _LastUpdate: + cutout_fixtures: List[models.CutoutFixture] + timestamp: datetime diff --git a/robot-server/robot_server/deck_configuration/validation.py b/robot-server/robot_server/deck_configuration/validation.py new file mode 100644 index 00000000000..0530a4f9271 --- /dev/null +++ b/robot-server/robot_server/deck_configuration/validation.py @@ -0,0 +1,122 @@ +"""Validate a deck configuration.""" + + +from collections import defaultdict +from dataclasses import dataclass +from typing import DefaultDict, FrozenSet, List, Set, Tuple, Union + +from opentrons_shared_data.deck import dev_types as deck_types + + +@dataclass(frozen=True) +class Placement: + """A placement of a cutout fixture in a cutout.""" + + cutout_id: str + cutout_fixture_id: str + + +@dataclass(frozen=True) +class UnoccupiedCutoutError: + """When a cutout has been left empty--no cutout fixtures mounted to it.""" + + cutout_id: str + + +@dataclass(frozen=True) +class OvercrowdedCutoutError: + """When a cutout has had more than one cutout fixture mounted to it.""" + + cutout_id: str + cutout_fixture_ids: Tuple[str, ...] + """All the conflicting cutout fixtures, in input order.""" + + +@dataclass(frozen=True) +class InvalidLocationError: + """When a cutout fixture has been mounted somewhere it cannot be mounted.""" + + cutout_id: str + cutout_fixture_id: str + allowed_cutout_ids: FrozenSet[str] + + +@dataclass(frozen=True) +class UnrecognizedCutoutFixtureError: + """When an cutout fixture has been mounted that's not defined by the deck definition.""" + + cutout_fixture_id: str + allowed_cutout_fixture_ids: FrozenSet[str] + + +ConfigurationError = Union[ + UnoccupiedCutoutError, + OvercrowdedCutoutError, + InvalidLocationError, + UnrecognizedCutoutFixtureError, +] + + +def get_configuration_errors( + deck_definition: deck_types.DeckDefinitionV4, + placements: List[Placement], +) -> Set[ConfigurationError]: + """Return all the problems with the given deck configration. + + If there are no problems, return ``{}``. + """ + errors: Set[ConfigurationError] = set() + fixtures_by_cutout: DefaultDict[str, List[str]] = defaultdict(list) + + for placement in placements: + fixtures_by_cutout[placement.cutout_id].append(placement.cutout_fixture_id) + + expected_cutouts = set(c["id"] for c in deck_definition["locations"]["cutouts"]) + occupied_cutouts = set(fixtures_by_cutout.keys()) + errors.update( + UnoccupiedCutoutError(cutout_id) + for cutout_id in expected_cutouts - occupied_cutouts + ) + + for cutout, fixtures in fixtures_by_cutout.items(): + if len(fixtures) > 1: + errors.add(OvercrowdedCutoutError(cutout, tuple(fixtures))) + + for placement in placements: + found_cutout_fixture = _find_cutout_fixture( + deck_definition, placement.cutout_fixture_id + ) + if isinstance(found_cutout_fixture, UnrecognizedCutoutFixtureError): + errors.add(found_cutout_fixture) + else: + allowed_cutout_ids = frozenset(found_cutout_fixture["mayMountTo"]) + if placement.cutout_id not in allowed_cutout_ids: + errors.add( + InvalidLocationError( + cutout_id=placement.cutout_id, + cutout_fixture_id=placement.cutout_fixture_id, + allowed_cutout_ids=allowed_cutout_ids, + ) + ) + + return errors + + +def _find_cutout_fixture( + deck_definition: deck_types.DeckDefinitionV4, cutout_fixture_id: str +) -> Union[deck_types.CutoutFixture, UnrecognizedCutoutFixtureError]: + cutout_fixtures = deck_definition["cutoutFixtures"] + try: + return next( + cutout_fixture + for cutout_fixture in cutout_fixtures + if cutout_fixture["id"] == cutout_fixture_id + ) + except StopIteration: # No match found. + allowed_cutout_fixture_ids = frozenset( + cutout_fixture["id"] for cutout_fixture in cutout_fixtures + ) + return UnrecognizedCutoutFixtureError( + cutout_fixture_id=cutout_fixture_id, + allowed_cutout_fixture_ids=allowed_cutout_fixture_ids, + ) diff --git a/robot-server/robot_server/deck_configuration/validation_mapping.py b/robot-server/robot_server/deck_configuration/validation_mapping.py new file mode 100644 index 00000000000..10d9b65158a --- /dev/null +++ b/robot-server/robot_server/deck_configuration/validation_mapping.py @@ -0,0 +1,39 @@ +"""Convert between internal types for validation and HTTP-exposed response models.""" + +import dataclasses +from typing import Iterable, List + +from . import models +from . import validation + + +def map_in(request: models.DeckConfigurationRequest) -> List[validation.Placement]: + """Map a request from HTTP to internal types that can be validated.""" + return [ + validation.Placement(cutout_id=p.cutoutId, cutout_fixture_id=p.cutoutFixtureId) + for p in request.cutoutFixtures + ] + + +def map_out( + validation_errors: Iterable[validation.ConfigurationError], +) -> List[models.InvalidDeckConfiguration]: + """Map internal results from validation to HTTP-exposed types.""" + return [_map_out_single_error(e) for e in validation_errors] + + +def _map_out_single_error( + error: validation.ConfigurationError, +) -> models.InvalidDeckConfiguration: + # Expose the error details in a developer-facing, kind of lazy way. + # This format isn't guaranteed by robot-server's HTTP API; + # it's just meant to help app developers debug their deck config requests. + meta = { + "deckConfigurationProblem": error.__class__.__name__, + # Note that this dataclasses.asdict() will break if the internal error + # that we're mapping from is ever not a dataclass. + **dataclasses.asdict(error), + } + return models.InvalidDeckConfiguration( + detail="Invalid deck configuration.", meta=meta + ) diff --git a/robot-server/robot_server/hardware.py b/robot-server/robot_server/hardware.py index 6af230bd9ff..760580fcef2 100644 --- a/robot-server/robot_server/hardware.py +++ b/robot-server/robot_server/hardware.py @@ -16,6 +16,8 @@ from uuid import uuid4 # direct to avoid import cycles in service.dependencies from traceback import format_exception_only, TracebackException from contextlib import contextmanager + +from opentrons_shared_data import deck from opentrons_shared_data.robot.dev_types import RobotType, RobotTypeEnum from opentrons import initialize as initialize_api, should_use_ot3 @@ -284,6 +286,13 @@ async def get_deck_type() -> DeckType: return DeckType(guess_deck_type_from_global_config()) +async def get_deck_definition( + deck_type: DeckType = Depends(get_deck_type), +) -> deck.dev_types.DeckDefinitionV4: + """Return this robot's deck definition.""" + return deck.load(deck_type, version=4) + + async def _postinit_ot2_tasks( hardware: ThreadManagedHardware, app_state: AppState, diff --git a/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py b/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py index 166faf7767e..e7935b44e83 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_engine_store.py @@ -24,6 +24,8 @@ create_protocol_engine, ) +from opentrons.protocol_engine.types import DeckConfigurationType + class EngineConflictError(RuntimeError): """An error raised if an active engine is already initialized. @@ -125,6 +127,7 @@ async def create( run_id: str, created_at: datetime, labware_offsets: List[LabwareOffsetCreate], + deck_configuration: Optional[DeckConfigurationType] = [], ) -> StateSummary: """Create and store a ProtocolRunner and ProtocolEngine for a given Run. @@ -150,6 +153,7 @@ async def create( RobotTypeEnum.robot_literal_to_enum(self._robot_type) ), ), + deck_configuration=deck_configuration, ) # Using LiveRunner as the runner to allow for future refactor of maintenance runs diff --git a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py index ccbfc07ef32..563916a7d16 100644 --- a/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py +++ b/robot-server/robot_server/maintenance_runs/maintenance_run_data_manager.py @@ -14,6 +14,8 @@ from .maintenance_engine_store import MaintenanceEngineStore from .maintenance_run_models import MaintenanceRun, MaintenanceRunNotFoundError +from opentrons.protocol_engine.types import DeckConfigurationType + def _build_run( run_id: str, @@ -73,6 +75,7 @@ async def create( run_id: str, created_at: datetime, labware_offsets: List[LabwareOffsetCreate], + deck_configuration: DeckConfigurationType, ) -> MaintenanceRun: """Create a new, current maintenance run. @@ -91,6 +94,7 @@ async def create( run_id=run_id, created_at=created_at, labware_offsets=labware_offsets, + deck_configuration=deck_configuration, ) return _build_run( diff --git a/robot-server/robot_server/maintenance_runs/router/base_router.py b/robot-server/robot_server/maintenance_runs/router/base_router.py index a37c1e66ba2..93c448e8390 100644 --- a/robot-server/robot_server/maintenance_runs/router/base_router.py +++ b/robot-server/robot_server/maintenance_runs/router/base_router.py @@ -35,6 +35,10 @@ from ..maintenance_run_data_manager import MaintenanceRunDataManager from ..dependencies import get_maintenance_run_data_manager +from robot_server.deck_configuration.fastapi_dependencies import ( + get_deck_configuration_store, +) +from robot_server.deck_configuration.store import DeckConfigurationStore log = logging.getLogger(__name__) base_router = APIRouter() @@ -147,6 +151,9 @@ async def create_run( get_is_okay_to_create_maintenance_run ), check_estop: bool = Depends(require_estop_in_good_state), + deck_configuration_store: DeckConfigurationStore = Depends( + get_deck_configuration_store + ), ) -> PydanticResponse[SimpleBody[MaintenanceRun]]: """Create a new maintenance run. @@ -157,6 +164,7 @@ async def create_run( created_at: Timestamp to attach to created run. is_ok_to_create_maintenance_run: Verify if a maintenance run may be created if a protocol run exists. check_estop: Dependency to verify the estop is in a valid state. + deck_configuration_store: Dependency to fetch the deck configuration. """ if not is_ok_to_create_maintenance_run: raise ProtocolRunIsActive( @@ -164,10 +172,13 @@ async def create_run( ).as_error(status.HTTP_409_CONFLICT) offsets = request_body.data.labwareOffsets if request_body is not None else [] + deck_configuration = await deck_configuration_store.get_deck_configuration() + run_data = await run_data_manager.create( run_id=run_id, created_at=created_at, labware_offsets=offsets, + deck_configuration=deck_configuration, ) log.info(f'Created an empty run "{run_id}"".') diff --git a/robot-server/robot_server/persistence/__init__.py b/robot-server/robot_server/persistence/__init__.py index 6a36022788a..e220ec29735 100644 --- a/robot-server/robot_server/persistence/__init__.py +++ b/robot-server/robot_server/persistence/__init__.py @@ -1,4 +1,4 @@ -"""Data access initialization and management.""" +"""Support for persisting data across device reboots.""" from ._database import create_sql_engine, sqlite_rowid diff --git a/robot-server/robot_server/protocols/analysis_models.py b/robot-server/robot_server/protocols/analysis_models.py index d31bb9f4eaa..8caa22f30a7 100644 --- a/robot-server/robot_server/protocols/analysis_models.py +++ b/robot-server/robot_server/protocols/analysis_models.py @@ -1,8 +1,9 @@ """Response models for protocol analysis.""" # TODO(mc, 2021-08-25): add modules to simulation result from enum import Enum +from opentrons_shared_data.robot.dev_types import RobotType from pydantic import BaseModel, Field -from typing import List, Union +from typing import List, Optional, Union from typing_extensions import Literal from opentrons.protocol_engine import ( @@ -75,6 +76,10 @@ class CompletedAnalysis(BaseModel): JSON protocols are currently deterministic by design. """ + # We want to unify this HTTP-facing analysis model with the one that local analysis returns. + # Until that happens, we need to keep these fields in sync manually. + + # Fields that are currently unique to robot-server, missing from local analysis: id: str = Field(..., description="Unique identifier of this analysis resource") status: Literal[AnalysisStatus.COMPLETED] = Field( AnalysisStatus.COMPLETED, @@ -84,9 +89,22 @@ class CompletedAnalysis(BaseModel): ..., description="Whether the protocol is expected to run successfully", ) - pipettes: List[LoadedPipette] = Field( + + # Fields that should match local analysis: + robotType: Optional[RobotType] = Field( + # robotType is deliberately typed as a Literal instead of an Enum. + # It's a bad idea at the moment to store enums in robot-server's database. + # https://opentrons.atlassian.net/browse/RSS-98 + default=None, # default=None to fit objects that were stored before this field existed. + description=( + "The type of robot that this protocol can run on." + " This field was added in v7.1.0. It will be `null` or omitted" + " in analyses that were originally created on older versions." + ), + ) + commands: List[Command] = Field( ..., - description="Pipettes used by the protocol", + description="The protocol commands the run is expected to produce", ) labware: List[LoadedLabware] = Field( ..., @@ -98,13 +116,17 @@ class CompletedAnalysis(BaseModel): " not its *initial* location." ), ) + pipettes: List[LoadedPipette] = Field( + ..., + description="Pipettes used by the protocol", + ) modules: List[LoadedModule] = Field( default_factory=list, description="Modules that have been loaded into the run.", ) - commands: List[Command] = Field( - ..., - description="The protocol commands the run is expected to produce", + liquids: List[Liquid] = Field( + default_factory=list, + description="Liquids used by the protocol", ) errors: List[ErrorOccurrence] = Field( ..., @@ -114,10 +136,6 @@ class CompletedAnalysis(BaseModel): " but it won't have more than one element." ), ) - liquids: List[Liquid] = Field( - default_factory=list, - description="Liquids used by the protocol", - ) ProtocolAnalysis = Union[PendingAnalysis, CompletedAnalysis] diff --git a/robot-server/robot_server/protocols/analysis_store.py b/robot-server/robot_server/protocols/analysis_store.py index 29ddb0d82ec..8165cf9cf8e 100644 --- a/robot-server/robot_server/protocols/analysis_store.py +++ b/robot-server/robot_server/protocols/analysis_store.py @@ -5,6 +5,7 @@ from logging import getLogger from typing import Dict, List, Optional from typing_extensions import Final +from opentrons_shared_data.robot.dev_types import RobotType import sqlalchemy @@ -120,6 +121,7 @@ def add_pending(self, protocol_id: str, analysis_id: str) -> AnalysisSummary: async def update( self, analysis_id: str, + robot_type: RobotType, commands: List[Command], labware: List[LoadedLabware], modules: List[LoadedModule], @@ -132,6 +134,7 @@ async def update( Args: analysis_id: The ID of the analysis to promote. Must point to a valid pending analysis. + robot_type: See `CompletedAnalysis.robotType`. commands: See `CompletedAnalysis.commands`. labware: See `CompletedAnalysis.labware`. modules: See `CompletedAnalysis.modules`. @@ -156,6 +159,7 @@ async def update( completed_analysis = CompletedAnalysis.construct( id=analysis_id, result=result, + robotType=robot_type, commands=commands, labware=labware, modules=modules, diff --git a/robot-server/robot_server/protocols/protocol_analyzer.py b/robot-server/robot_server/protocols/protocol_analyzer.py index cd8c44cb539..542ece91284 100644 --- a/robot-server/robot_server/protocols/protocol_analyzer.py +++ b/robot-server/robot_server/protocols/protocol_analyzer.py @@ -30,12 +30,15 @@ async def analyze( robot_type=protocol_resource.source.robot_type, protocol_config=protocol_resource.source.config, ) - result = await runner.run(protocol_resource.source) + result = await runner.run( + protocol_source=protocol_resource.source, deck_configuration=[] + ) log.info(f'Completed analysis "{analysis_id}".') await self._analysis_store.update( analysis_id=analysis_id, + robot_type=protocol_resource.source.robot_type, commands=result.commands, labware=result.state_summary.labware, modules=result.state_summary.modules, diff --git a/robot-server/robot_server/robot/calibration/tip_length/user_flow.py b/robot-server/robot_server/robot/calibration/tip_length/user_flow.py index 11811ea339a..e771c0def5d 100644 --- a/robot-server/robot_server/robot/calibration/tip_length/user_flow.py +++ b/robot-server/robot_server/robot/calibration/tip_length/user_flow.py @@ -82,6 +82,7 @@ def __init__( cast(List[LabwareUri], self.hw_pipette.liquid_class.default_tipracks) ) self._supported_commands = SupportedCommands(namespace="calibration") + self._supported_commands.loadLabware = True def _set_current_state(self, to_state: State): self._current_state = to_state @@ -168,7 +169,13 @@ async def load_labware( self, tiprackDefinition: Optional[LabwareDefinition] = None, ): - pass + self._supported_commands.loadLabware = False + if tiprackDefinition: + verified_definition = labware.verify_definition(tiprackDefinition) + self._tip_rack = self._get_tip_rack_lw(verified_definition) + if self._deck[TIP_RACK_SLOT]: + del self._deck[TIP_RACK_SLOT] + self._deck[TIP_RACK_SLOT] = self._tip_rack async def move_to_tip_rack(self): await self._move(Location(self.tip_origin, None)) diff --git a/robot-server/robot_server/robot/calibration/util.py b/robot-server/robot_server/robot/calibration/util.py index d7a1d2a7845..f07f6852e68 100644 --- a/robot-server/robot_server/robot/calibration/util.py +++ b/robot-server/robot_server/robot/calibration/util.py @@ -1,8 +1,6 @@ import logging -import contextlib from typing import Set, Dict, Any, Union, List, Optional, TYPE_CHECKING -from opentrons.hardware_control.instruments.ot2.pipette import Pipette from opentrons.hardware_control.util import plan_arc from opentrons.hardware_control.types import CriticalPoint from opentrons.protocol_api import labware @@ -108,33 +106,26 @@ def get_next_state(self, from_state, command): async def invalidate_tip(user_flow: CalibrationUserFlow): await user_flow.return_tip() user_flow.reset_tip_origin() + await user_flow.hardware.update_nozzle_configuration_for_mount( + user_flow.mount, None, None + ) await user_flow.move_to_tip_rack() -@contextlib.contextmanager -def save_default_pick_up_current(instr: Pipette): - # reduce pick up current for multichannel pipette picking up 1 tip - saved_default = instr.pick_up_configurations.current - instr.update_config_item({"pick_up_current": 0.1}) - - try: - yield - finally: - instr.update_config_item({"pick_up_current": saved_default}) - - async def pick_up_tip(user_flow: CalibrationUserFlow, tip_length: float): # grab position of active nozzle for ref when returning tip later cp = user_flow.critical_point_override user_flow.tip_origin = await user_flow.hardware.gantry_position( user_flow.mount, critical_point=cp ) - - with contextlib.ExitStack() as stack: - if user_flow.hw_pipette.config.channels > 1: - stack.enter_context(save_default_pick_up_current(user_flow.hw_pipette)) - - await user_flow.hardware.pick_up_tip(user_flow.mount, tip_length) + if user_flow.hw_pipette.config.channels > 1: + await user_flow.hardware.update_nozzle_configuration_for_mount( + user_flow.mount, + back_left_nozzle="H1", + front_right_nozzle="H1", + starting_nozzle="H1", + ) + await user_flow.hardware.pick_up_tip(user_flow.mount, tip_length) async def return_tip(user_flow: CalibrationUserFlow, tip_length: float): @@ -154,6 +145,9 @@ async def return_tip(user_flow: CalibrationUserFlow, tip_length: float): ) await user_flow.hardware.drop_tip(user_flow.mount) user_flow.reset_tip_origin() + await user_flow.hardware.update_nozzle_configuration_for_mount( + user_flow.mount, None, None + ) async def move( diff --git a/robot-server/robot_server/router.py b/robot-server/robot_server/router.py index 0ef30d30a2c..76449fb220a 100644 --- a/robot-server/robot_server/router.py +++ b/robot-server/robot_server/router.py @@ -3,23 +3,25 @@ from .constants import V1_TAG from .errors import LegacyErrorResponse +from .versioning import check_version_header + +from .commands import commands_router +from .deck_configuration.router import router as deck_configuration_router from .health import health_router -from .protocols import protocols_router -from .runs import runs_router +from .instruments import instruments_router from .maintenance_runs.router import maintenance_runs_router -from .commands import commands_router from .modules import modules_router -from .instruments import instruments_router -from .system import system_router -from .versioning import check_version_header +from .protocols import protocols_router +from .robot.router import robot_router +from .runs import runs_router +from .service.labware.router import router as labware_router from .service.legacy.routers import legacy_routes -from .service.session.router import router as deprecated_session_router +from .service.notifications.router import router as notifications_router from .service.pipette_offset.router import router as pip_os_router -from .service.labware.router import router as labware_router +from .service.session.router import router as deprecated_session_router from .service.tip_length.router import router as tl_router -from .service.notifications.router import router as notifications_router from .subsystems.router import subsystems_router -from .robot.router import robot_router +from .system import system_router router = APIRouter() @@ -69,6 +71,12 @@ dependencies=[Depends(check_version_header)], ) +router.include_router( + router=deck_configuration_router, + tags=["Deck Configuration"], + dependencies=[Depends(check_version_header)], +) + router.include_router( router=modules_router, tags=["Attached Modules"], @@ -77,7 +85,7 @@ router.include_router( router=instruments_router, - tags=["Attached instruments"], + tags=["Attached Instruments"], dependencies=[Depends(check_version_header)], ) diff --git a/robot-server/robot_server/runs/engine_store.py b/robot-server/robot_server/runs/engine_store.py index 558a903332d..056f8d55259 100644 --- a/robot-server/robot_server/runs/engine_store.py +++ b/robot-server/robot_server/runs/engine_store.py @@ -32,6 +32,7 @@ ) from robot_server.protocols import ProtocolResource +from opentrons.protocol_engine.types import DeckConfigurationType class EngineConflictError(RuntimeError): @@ -150,6 +151,7 @@ async def create( self, run_id: str, labware_offsets: List[LabwareOffsetCreate], + deck_configuration: DeckConfigurationType, protocol: Optional[ProtocolResource], ) -> StateSummary: """Create and store a ProtocolRunner and ProtocolEngine for a given Run. @@ -181,6 +183,7 @@ async def create( ), ), load_fixed_trash=load_fixed_trash, + deck_configuration=deck_configuration, ) runner = create_protocol_runner( protocol_engine=engine, diff --git a/robot-server/robot_server/runs/router/actions_router.py b/robot-server/robot_server/runs/router/actions_router.py index 5ac51ffd09f..dc3f8ab4aba 100644 --- a/robot-server/robot_server/runs/router/actions_router.py +++ b/robot-server/robot_server/runs/router/actions_router.py @@ -11,6 +11,11 @@ from robot_server.service.json_api import RequestModel, SimpleBody, PydanticResponse from robot_server.service.task_runner import TaskRunner, get_task_runner from robot_server.robot.control.dependencies import require_estop_in_good_state +from robot_server.deck_configuration.fastapi_dependencies import ( + get_deck_configuration_store, +) +from robot_server.deck_configuration.store import DeckConfigurationStore +from opentrons.protocol_engine.types import DeckConfigurationType from ..engine_store import EngineStore from ..run_store import RunStore @@ -82,12 +87,16 @@ async def get_run_controller( async def create_run_action( runId: str, request_body: RequestModel[RunActionCreate], + engine_store: EngineStore = Depends(get_engine_store), run_controller: RunController = Depends(get_run_controller), action_id: str = Depends(get_unique_id), created_at: datetime = Depends(get_current_time), maintenance_engine_store: MaintenanceEngineStore = Depends( get_maintenance_engine_store ), + deck_configuration_store: DeckConfigurationStore = Depends( + get_deck_configuration_store + ), check_estop: bool = Depends(require_estop_in_good_state), ) -> PydanticResponse[SimpleBody[RunAction]]: """Create a run control action. @@ -99,11 +108,14 @@ async def create_run_action( Arguments: runId: Run ID pulled from the URL. request_body: Input payload from the request body. + engine_store: Dependency to fetch the engine store. run_controller: Run controller bound to the given run ID. action_id: Generated ID to assign to the control action. created_at: Timestamp to attach to the control action. maintenance_engine_store: The maintenance run's EngineStore + deck_configuration_store: The deck configuration store check_estop: Dependency to verify the estop is in a valid state. + deck_configuration_store: Dependency to fetch the deck configuration. """ action_type = request_body.data.actionType if ( @@ -112,10 +124,14 @@ async def create_run_action( ): await maintenance_engine_store.clear() try: + deck_configuration: DeckConfigurationType = [] + if action_type == RunActionType.PLAY: + deck_configuration = await deck_configuration_store.get_deck_configuration() action = run_controller.create_action( action_id=action_id, action_type=action_type, created_at=created_at, + action_payload=deck_configuration, ) except RunActionNotAllowedError as e: diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index 36978003c52..43b1202d29b 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -41,6 +41,11 @@ from ..run_data_manager import RunDataManager, RunNotCurrentError from ..dependencies import get_run_data_manager, get_run_auto_deleter +from robot_server.deck_configuration.fastapi_dependencies import ( + get_deck_configuration_store, +) +from robot_server.deck_configuration.store import DeckConfigurationStore + log = logging.getLogger(__name__) base_router = APIRouter() @@ -135,6 +140,9 @@ async def create_run( created_at: datetime = Depends(get_current_time), run_auto_deleter: RunAutoDeleter = Depends(get_run_auto_deleter), check_estop: bool = Depends(require_estop_in_good_state), + deck_configuration_store: DeckConfigurationStore = Depends( + get_deck_configuration_store + ), ) -> PydanticResponse[SimpleBody[Run]]: """Create a new run. @@ -147,11 +155,14 @@ async def create_run( run_auto_deleter: An interface to delete old resources to make room for the new run. check_estop: Dependency to verify the estop is in a valid state. + deck_configuration_store: Dependency to fetch the deck configuration. """ protocol_id = request_body.data.protocolId if request_body is not None else None offsets = request_body.data.labwareOffsets if request_body is not None else [] protocol_resource = None + deck_configuration = await deck_configuration_store.get_deck_configuration() + # TODO (tz, 5-16-22): same error raised twice. # Check if we can consolidate to one place. if protocol_id is not None: @@ -170,6 +181,7 @@ async def create_run( run_id=run_id, created_at=created_at, labware_offsets=offsets, + deck_configuration=deck_configuration, protocol=protocol_resource, ) except EngineConflictError as e: diff --git a/robot-server/robot_server/runs/run_controller.py b/robot-server/robot_server/runs/run_controller.py index 6a75c1a3131..66ff5210081 100644 --- a/robot-server/robot_server/runs/run_controller.py +++ b/robot-server/robot_server/runs/run_controller.py @@ -1,7 +1,7 @@ """Control an active run with Actions.""" import logging from datetime import datetime - +from typing import Optional from opentrons.protocol_engine import ProtocolEngineError from opentrons_shared_data.errors.exceptions import RoboticsInteractionError @@ -11,6 +11,7 @@ from .run_store import RunStore from .action_models import RunAction, RunActionType +from opentrons.protocol_engine.types import DeckConfigurationType log = logging.getLogger(__name__) @@ -39,6 +40,7 @@ def create_action( action_id: str, action_type: RunActionType, created_at: datetime, + action_payload: Optional[DeckConfigurationType], ) -> RunAction: """Create a run action. @@ -69,7 +71,11 @@ def create_action( # TODO(mc, 2022-05-13): engine_store.runner.run could raise # the same errors as runner.play, but we are unable to catch them. # This unlikely to occur in production, but should be addressed. - self._task_runner.run(self._run_protocol_and_insert_result) + + self._task_runner.run( + func=self._run_protocol_and_insert_result, + deck_configuration=action_payload, + ) elif action_type == RunActionType.PAUSE: log.info(f'Pausing run "{self._run_id}".') @@ -84,10 +90,15 @@ def create_action( self._run_store.insert_action(run_id=self._run_id, action=action) + # TODO (spp, 2023-11-09): I think the response should also containt the action payload return action - async def _run_protocol_and_insert_result(self) -> None: - result = await self._engine_store.runner.run() + async def _run_protocol_and_insert_result( + self, deck_configuration: DeckConfigurationType + ) -> None: + result = await self._engine_store.runner.run( + deck_configuration=deck_configuration + ) self._run_store.update_run_state( run_id=self._run_id, summary=result.state_summary, diff --git a/robot-server/robot_server/runs/run_data_manager.py b/robot-server/robot_server/runs/run_data_manager.py index 086bd3a94a8..9954e33d8d3 100644 --- a/robot-server/robot_server/runs/run_data_manager.py +++ b/robot-server/robot_server/runs/run_data_manager.py @@ -20,6 +20,8 @@ from .run_store import RunResource, RunStore from .run_models import Run +from opentrons.protocol_engine.types import DeckConfigurationType + def _build_run( run_resource: RunResource, @@ -87,6 +89,7 @@ async def create( run_id: str, created_at: datetime, labware_offsets: List[LabwareOffsetCreate], + deck_configuration: DeckConfigurationType, protocol: Optional[ProtocolResource], ) -> Run: """Create a new, current run. @@ -115,6 +118,7 @@ async def create( state_summary = await self._engine_store.create( run_id=run_id, labware_offsets=labware_offsets, + deck_configuration=deck_configuration, protocol=protocol, ) run_resource = self._run_store.insert( diff --git a/robot-server/robot_server/runs/run_store.py b/robot-server/robot_server/runs/run_store.py index 32f2eca3251..f371cc8e67d 100644 --- a/robot-server/robot_server/runs/run_store.py +++ b/robot-server/robot_server/runs/run_store.py @@ -1,4 +1,5 @@ """Runs' on-db store.""" +import logging from collections import defaultdict from dataclasses import dataclass from datetime import datetime @@ -6,7 +7,7 @@ from typing import Any, Dict, List, Optional, cast import sqlalchemy -from pydantic import parse_obj_as +from pydantic import parse_obj_as, ValidationError from opentrons.util.helpers import utc_now from opentrons.protocol_engine import StateSummary, CommandSlice @@ -18,6 +19,8 @@ from .action_models import RunAction, RunActionType from .run_models import RunNotFoundError +log = logging.getLogger(__name__) + _CACHE_ENTRIES = 32 @@ -256,11 +259,15 @@ def get_state_summary(self, run_id: str) -> Optional[StateSummary]: with self._sql_engine.begin() as transaction: row = transaction.execute(select_run_data).one() - return ( - StateSummary.parse_obj(row.state_summary) - if row.state_summary is not None - else None - ) + try: + return ( + StateSummary.parse_obj(row.state_summary) + if row.state_summary is not None + else None + ) + except ValidationError as e: + log.warning(f"Error retrieving state summary for {run_id}: {e}") + return None @lru_cache(maxsize=_CACHE_ENTRIES) def _get_all_unparsed_commands(self, run_id: str) -> List[Dict[str, Any]]: diff --git a/robot-server/tests/deck_configuration/test_defaults.py b/robot-server/tests/deck_configuration/test_defaults.py new file mode 100644 index 00000000000..ec3bbed3c22 --- /dev/null +++ b/robot-server/tests/deck_configuration/test_defaults.py @@ -0,0 +1,30 @@ +"""Unit tests for robot_server.deck_configuration.defaults.""" + + +from typing_extensions import Final + +import pytest + +from opentrons_shared_data import deck + +from robot_server.deck_configuration import defaults as subject +from robot_server.deck_configuration import validation +from robot_server.deck_configuration import validation_mapping + + +DECK_DEFINITION_VERSION: Final = 4 + + +@pytest.mark.parametrize( + "deck_definition_name", deck.list_names(DECK_DEFINITION_VERSION) +) +def test_defaults(deck_definition_name: str) -> None: + """Make sure there's a valid default for every possible deck definition.""" + deck_definition = deck.load(deck_definition_name, DECK_DEFINITION_VERSION) + result = subject.for_deck_definition(deck_definition_name) + assert ( + validation.get_configuration_errors( + deck_definition, validation_mapping.map_in(result) + ) + == set() + ) diff --git a/robot-server/tests/deck_configuration/test_validation.py b/robot-server/tests/deck_configuration/test_validation.py new file mode 100644 index 00000000000..5aee74491da --- /dev/null +++ b/robot-server/tests/deck_configuration/test_validation.py @@ -0,0 +1,203 @@ +"""Unit tests for robot_server.deck_configuration.validation.""" + +from opentrons_shared_data.deck import load as load_deck_definition + +from robot_server.deck_configuration import validation as subject + + +def test_valid() -> None: + """It should return an empty error list if the input is valid.""" + deck_definition = load_deck_definition("ot3_standard", version=4) + cutout_fixtures = [ + subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) + for cutout_fixture_id, cutout_id in [ + ("singleLeftSlot", "cutoutA1"), + ("singleLeftSlot", "cutoutB1"), + ("singleLeftSlot", "cutoutC1"), + ("singleLeftSlot", "cutoutD1"), + ("singleCenterSlot", "cutoutA2"), + ("singleCenterSlot", "cutoutB2"), + ("singleCenterSlot", "cutoutC2"), + ("singleCenterSlot", "cutoutD2"), + ("stagingAreaRightSlot", "cutoutA3"), + ("singleRightSlot", "cutoutB3"), + ("stagingAreaRightSlot", "cutoutC3"), + ("singleRightSlot", "cutoutD3"), + ] + ] + assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == set() + + +def test_invalid_empty_cutouts() -> None: + """It should enforce that every cutout is occupied.""" + deck_definition = load_deck_definition("ot3_standard", version=4) + cutout_fixtures = [ + subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) + for cutout_fixture_id, cutout_id in [ + ("singleLeftSlot", "cutoutA1"), + ("singleLeftSlot", "cutoutB1"), + ("singleLeftSlot", "cutoutC1"), + ("singleLeftSlot", "cutoutD1"), + ("singleCenterSlot", "cutoutA2"), + ("singleCenterSlot", "cutoutB2"), + # Invalid because we haven't placed anything into cutout C2 or D2. + # ("singleCenterSlot", "cutoutC2"), + # ("singleCenterSlot", "cutoutD2"), + ("stagingAreaRightSlot", "cutoutA3"), + ("singleRightSlot", "cutoutB3"), + ("stagingAreaRightSlot", "cutoutC3"), + ("singleRightSlot", "cutoutD3"), + ] + ] + assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { + subject.UnoccupiedCutoutError(cutout_id="cutoutC2"), + subject.UnoccupiedCutoutError(cutout_id="cutoutD2"), + } + + +def test_invalid_overcrowded_cutouts() -> None: + """It should prevent you from putting multiple things into a single cutout.""" + deck_definition = load_deck_definition("ot3_standard", version=4) + cutout_fixtures = [ + subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) + for cutout_fixture_id, cutout_id in [ + ("singleLeftSlot", "cutoutA1"), + ("singleLeftSlot", "cutoutB1"), + ("singleLeftSlot", "cutoutC1"), + ("singleLeftSlot", "cutoutD1"), + ("singleCenterSlot", "cutoutA2"), + ("singleCenterSlot", "cutoutB2"), + ("singleCenterSlot", "cutoutC2"), + ("singleCenterSlot", "cutoutD2"), + ("stagingAreaRightSlot", "cutoutA3"), + ("singleRightSlot", "cutoutB3"), + # Invalid because we're placing two things in cutout C3... + ("stagingAreaRightSlot", "cutoutC3"), + ("stagingAreaRightSlot", "cutoutC3"), + # ...and two things in cutout D3. + ("wasteChuteRightAdapterNoCover", "cutoutD3"), + ("singleRightSlot", "cutoutD3"), + ] + ] + assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { + subject.OvercrowdedCutoutError( + cutout_id="cutoutC3", + cutout_fixture_ids=("stagingAreaRightSlot", "stagingAreaRightSlot"), + ), + subject.OvercrowdedCutoutError( + cutout_id="cutoutD3", + cutout_fixture_ids=("wasteChuteRightAdapterNoCover", "singleRightSlot"), + ), + } + + +def test_invalid_cutout_for_fixture() -> None: + """Each fixture must be placed in a location that's valid for that particular fixture.""" + deck_definition = load_deck_definition("ot3_standard", version=4) + cutout_fixtures = [ + subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) + for cutout_fixture_id, cutout_id in [ + ("singleLeftSlot", "cutoutA1"), + ("singleLeftSlot", "cutoutB1"), + ("singleLeftSlot", "cutoutC1"), + ("singleLeftSlot", "cutoutD1"), + ("singleCenterSlot", "cutoutA2"), + ("singleCenterSlot", "cutoutB2"), + # Invalid because wasteChuteRightAdapterNoCover can't be placed in cutout C2... + ("wasteChuteRightAdapterNoCover", "cutoutC2"), + # ...nor can singleLeftSlot be placed in cutout D2. + ("singleLeftSlot", "cutoutD2"), + ("stagingAreaRightSlot", "cutoutA3"), + ("singleRightSlot", "cutoutB3"), + ("stagingAreaRightSlot", "cutoutC3"), + ("singleRightSlot", "cutoutD3"), + ] + ] + assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { + subject.InvalidLocationError( + cutout_id="cutoutC2", + cutout_fixture_id="wasteChuteRightAdapterNoCover", + allowed_cutout_ids=frozenset(["cutoutD3"]), + ), + subject.InvalidLocationError( + cutout_id="cutoutD2", + cutout_fixture_id="singleLeftSlot", + allowed_cutout_ids=frozenset( + ["cutoutA1", "cutoutB1", "cutoutC1", "cutoutD1"] + ), + ), + } + + +def test_unrecognized_cutout() -> None: + """It should raise a sensible error if you pass a totally nonexistent cutout.""" + deck_definition = load_deck_definition("ot3_standard", version=4) + cutout_fixtures = [ + subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) + for cutout_fixture_id, cutout_id in [ + ("singleLeftSlot", "cutoutA1"), + ("singleLeftSlot", "cutoutB1"), + ("singleLeftSlot", "cutoutC1"), + ("singleLeftSlot", "cutoutD1"), + ("singleCenterSlot", "cutoutA2"), + ("singleCenterSlot", "cutoutB2"), + ("singleCenterSlot", "cutoutC2"), + ("singleCenterSlot", "cutoutD2"), + ("singleRightSlot", "cutoutA3"), + ("singleRightSlot", "cutoutB3"), + ("singleRightSlot", "cutoutC3"), + ("singleRightSlot", "cutoutD3"), + # Invalid because "someUnrecognizedCutout" is not defined by the deck definition. + ("singleRightSlot", "someUnrecognizedCutout"), + ] + ] + assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { + subject.InvalidLocationError( + cutout_fixture_id="singleRightSlot", + cutout_id="someUnrecognizedCutout", + allowed_cutout_ids=frozenset( + ["cutoutA3", "cutoutB3", "cutoutC3", "cutoutD3"] + ), + ) + } + + +def test_unrecognized_cutout_fixture() -> None: + """It should raise a sensible error if you pass a totally nonexistent cutout fixture.""" + deck_definition = load_deck_definition("ot3_standard", version=4) + cutout_fixtures = [ + subject.Placement(cutout_fixture_id=cutout_fixture_id, cutout_id=cutout_id) + for cutout_fixture_id, cutout_id in [ + ("singleLeftSlot", "cutoutA1"), + ("singleLeftSlot", "cutoutB1"), + ("singleLeftSlot", "cutoutC1"), + ("singleLeftSlot", "cutoutD1"), + ("singleCenterSlot", "cutoutA2"), + ("singleCenterSlot", "cutoutB2"), + ("singleCenterSlot", "cutoutC2"), + ("singleCenterSlot", "cutoutD2"), + ("singleRightSlot", "cutoutA3"), + ("singleRightSlot", "cutoutB3"), + ("singleRightSlot", "cutoutC3"), + # Invalid because "someUnrecognizedCutoutFixture" is not defined by the deck definition. + ("someUnrecognizedCutoutFixture", "cutoutD3"), + ] + ] + assert subject.get_configuration_errors(deck_definition, cutout_fixtures) == { + subject.UnrecognizedCutoutFixtureError( + cutout_fixture_id="someUnrecognizedCutoutFixture", + allowed_cutout_fixture_ids=frozenset( + [ + "singleLeftSlot", + "singleCenterSlot", + "singleRightSlot", + "stagingAreaRightSlot", + "wasteChuteRightAdapterCovered", + "wasteChuteRightAdapterNoCover", + "stagingAreaSlotWithWasteChuteRightAdapterCovered", + "stagingAreaSlotWithWasteChuteRightAdapterNoCover", + "trashBinAdapter", + ] + ), + ) + } diff --git a/robot-server/tests/integration/conftest.py b/robot-server/tests/integration/conftest.py index 669e2cb7655..6ba77ffbe6b 100644 --- a/robot-server/tests/integration/conftest.py +++ b/robot-server/tests/integration/conftest.py @@ -133,6 +133,7 @@ def _wait_until_ready(base_url: str) -> None: time.sleep(0.1) +# TODO(mm, 2023-11-02): This should also restore the server's original deck configuration. def _clean_server_state(base_url: str) -> None: async def _clean_server_state_async() -> None: async with RobotClient.make(base_url=base_url, version="*") as robot_client: diff --git a/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml index d4ddc81b2eb..d84cc464cbc 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml @@ -84,6 +84,7 @@ stages: - id: '{analysis_id}' status: completed result: ok + robotType: OT-2 Standard pipettes: - id: pipetteId pipetteName: p10_single diff --git a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml index 7165f65d478..a2ec1a8bb6a 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml @@ -85,6 +85,7 @@ stages: - id: '{analysis_id}' status: completed result: ok + robotType: OT-3 Standard pipettes: - id: pipetteId pipetteName: p1000_96 diff --git a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml index e33c1d71026..954551ebd53 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml @@ -84,6 +84,7 @@ stages: - id: '{analysis_id}' status: completed result: ok + robotType: OT-2 Standard pipettes: - id: pipetteId pipetteName: p10_single diff --git a/robot-server/tests/integration/http_api/test_deck_configuration.tavern.yaml b/robot-server/tests/integration/http_api/test_deck_configuration.tavern.yaml new file mode 100644 index 00000000000..59253102b8c --- /dev/null +++ b/robot-server/tests/integration/http_api/test_deck_configuration.tavern.yaml @@ -0,0 +1,146 @@ +test_name: Test setting and getting a Flex deck configuration + +marks: + - ot3_only + - usefixtures: + - ot3_server_base_url + +stages: + - name: Get the deck configuration and make sure there's a default + request: + url: '{ot3_server_base_url}/deck_configuration' + response: + json: + data: + # lastUpdatedAt is deliberately omitted from this expected object. + # A lastUpdatedAt that's omitted or null means the deck configuration has never been set. + # + # Unfortunately, this makes this test order-dependent with any other tests + # that modify the deck configuration, even if they try to restore the original value + # after they're done. We probably need some kind of deck configuration factory-reset. + # + # lastUpdatedAt: null + cutoutFixtures: + - cutoutFixtureId: singleLeftSlot + cutoutId: cutoutA1 + - cutoutFixtureId: singleLeftSlot + cutoutId: cutoutB1 + - cutoutFixtureId: singleLeftSlot + cutoutId: cutoutC1 + - cutoutFixtureId: singleLeftSlot + cutoutId: cutoutD1 + - cutoutFixtureId: singleCenterSlot + cutoutId: cutoutA2 + - cutoutFixtureId: singleCenterSlot + cutoutId: cutoutB2 + - cutoutFixtureId: singleCenterSlot + cutoutId: cutoutC2 + - cutoutFixtureId: singleCenterSlot + cutoutId: cutoutD2 + - cutoutFixtureId: trashBinAdapter + cutoutId: cutoutA3 + - cutoutFixtureId: singleRightSlot + cutoutId: cutoutB3 + - cutoutFixtureId: singleRightSlot + cutoutId: cutoutC3 + - cutoutFixtureId: singleRightSlot + cutoutId: cutoutD3 + + - name: Set a new, valid deck configuration + request: + url: '{ot3_server_base_url}/deck_configuration' + method: PUT + json: + data: + cutoutFixtures: &expectedCutoutFixtures + - cutoutFixtureId: singleLeftSlot + cutoutId: cutoutA1 + - cutoutFixtureId: singleLeftSlot + cutoutId: cutoutB1 + - cutoutFixtureId: singleLeftSlot + cutoutId: cutoutC1 + - cutoutFixtureId: singleLeftSlot + cutoutId: cutoutD1 + - cutoutFixtureId: singleCenterSlot + cutoutId: cutoutA2 + - cutoutFixtureId: singleCenterSlot + cutoutId: cutoutB2 + - cutoutFixtureId: singleCenterSlot + cutoutId: cutoutC2 + - cutoutFixtureId: singleCenterSlot + cutoutId: cutoutD2 + # Throw in a weird right-side deck layout + # so we know this won't happen to match the default. + - cutoutFixtureId: stagingAreaRightSlot + cutoutId: cutoutA3 + - cutoutFixtureId: singleRightSlot + cutoutId: cutoutB3 + - cutoutFixtureId: stagingAreaRightSlot + cutoutId: cutoutC3 + - cutoutFixtureId: singleRightSlot + cutoutId: cutoutD3 + + - name: Get the deck configuration and make sure it's the same one that we just set + request: + url: '{ot3_server_base_url}/deck_configuration' + response: + json: + data: + lastUpdatedAt: !anystr + cutoutFixtures: *expectedCutoutFixtures + save: + json: + last_updated_at: data.lastUpdatedAt + + - name: Set an invalid deck configuration + request: + url: '{ot3_server_base_url}/deck_configuration' + method: PUT + json: + data: + cutoutFixtures: + # Invalid deck configuration: cutoutA1 is left unoccupied. + # - cutoutFixtureId: singleLeftSlot + # cutoutId: cutoutA1 + - cutoutFixtureId: singleLeftSlot + cutoutId: cutoutB1 + - cutoutFixtureId: singleLeftSlot + cutoutId: cutoutC1 + - cutoutFixtureId: singleLeftSlot + cutoutId: cutoutD1 + - cutoutFixtureId: singleCenterSlot + cutoutId: cutoutA2 + - cutoutFixtureId: singleCenterSlot + cutoutId: cutoutB2 + - cutoutFixtureId: singleCenterSlot + cutoutId: cutoutC2 + - cutoutFixtureId: singleCenterSlot + cutoutId: cutoutD2 + - cutoutFixtureId: singleRightSlot + cutoutId: cutoutA3 + - cutoutFixtureId: singleRightSlot + cutoutId: cutoutB3 + - cutoutFixtureId: singleRightSlot + cutoutId: cutoutC3 + - cutoutFixtureId: singleRightSlot + cutoutId: cutoutD3 + response: + status_code: 422 + json: + errors: + - id: InvalidDeckConfiguration + title: Invalid Deck Configuration + errorCode: '4000' + detail: Invalid deck configuration. + meta: + deckConfigurationProblem: UnoccupiedCutoutError + cutout_id: cutoutA1 + + - name: Get the deck configuration and make sure it's not the invalid one + request: + url: '{ot3_server_base_url}/deck_configuration' + response: + json: + data: + lastUpdatedAt: '{last_updated_at}' + cutoutFixtures: *expectedCutoutFixtures diff --git a/robot-server/tests/maintenance_runs/router/conftest.py b/robot-server/tests/maintenance_runs/router/conftest.py index f71319e8dd4..e5796a31bba 100644 --- a/robot-server/tests/maintenance_runs/router/conftest.py +++ b/robot-server/tests/maintenance_runs/router/conftest.py @@ -9,6 +9,7 @@ MaintenanceRunDataManager, ) from opentrons.protocol_engine import ProtocolEngine +from robot_server.deck_configuration.store import DeckConfigurationStore @pytest.fixture() @@ -27,3 +28,9 @@ def mock_protocol_engine(decoy: Decoy) -> ProtocolEngine: def mock_maintenance_run_data_manager(decoy: Decoy) -> MaintenanceRunDataManager: """Get a mock RunDataManager.""" return decoy.mock(cls=MaintenanceRunDataManager) + + +@pytest.fixture +def mock_deck_configuration_store(decoy: Decoy) -> DeckConfigurationStore: + """Get a mock DeckConfigurationStore.""" + return decoy.mock(cls=DeckConfigurationStore) diff --git a/robot-server/tests/maintenance_runs/router/test_base_router.py b/robot-server/tests/maintenance_runs/router/test_base_router.py index 53a71b23c62..4f1c7b36efd 100644 --- a/robot-server/tests/maintenance_runs/router/test_base_router.py +++ b/robot-server/tests/maintenance_runs/router/test_base_router.py @@ -33,6 +33,8 @@ AllRunsLinks, ) +from robot_server.deck_configuration.store import DeckConfigurationStore + @pytest.fixture def labware_offset_create() -> LabwareOffsetCreate: @@ -48,6 +50,7 @@ async def test_create_run( decoy: Decoy, mock_maintenance_run_data_manager: MaintenanceRunDataManager, labware_offset_create: pe_types.LabwareOffsetCreate, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should be able to create a basic run.""" run_id = "run-id" @@ -67,11 +70,15 @@ async def test_create_run( liquids=[], ) + decoy.when( + await mock_deck_configuration_store.get_deck_configuration() + ).then_return([]) decoy.when( await mock_maintenance_run_data_manager.create( run_id=run_id, created_at=run_created_at, labware_offsets=[labware_offset_create], + deck_configuration=[], ) ).then_return(expected_response) @@ -83,6 +90,7 @@ async def test_create_run( run_id=run_id, created_at=run_created_at, is_ok_to_create_maintenance_run=True, + deck_configuration_store=mock_deck_configuration_store, ) assert result.content.data == expected_response @@ -92,10 +100,13 @@ async def test_create_run( async def test_create_maintenance_run_with_protocol_run_conflict( decoy: Decoy, mock_maintenance_run_data_manager: MaintenanceRunDataManager, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should respond with a conflict error if protocol run is active during maintenance run creation.""" created_at = datetime(year=2021, month=1, day=1) - + decoy.when( + await mock_deck_configuration_store.get_deck_configuration() + ).then_return([]) with pytest.raises(ApiError) as exc_info: await create_run( run_id="run-id", @@ -103,6 +114,7 @@ async def test_create_maintenance_run_with_protocol_run_conflict( request_body=None, run_data_manager=mock_maintenance_run_data_manager, is_ok_to_create_maintenance_run=False, + deck_configuration_store=mock_deck_configuration_store, ) assert exc_info.value.status_code == 409 assert exc_info.value.content["errors"][0]["id"] == "ProtocolRunIsActive" diff --git a/robot-server/tests/maintenance_runs/test_engine_store.py b/robot-server/tests/maintenance_runs/test_engine_store.py index 907c244e71d..d0a3ccfc1c8 100644 --- a/robot-server/tests/maintenance_runs/test_engine_store.py +++ b/robot-server/tests/maintenance_runs/test_engine_store.py @@ -106,7 +106,7 @@ async def test_clear_engine(subject: MaintenanceEngineStore) -> None: await subject.create( run_id="run-id", labware_offsets=[], created_at=datetime(2023, 5, 1) ) - await subject.runner.run() + await subject.runner.run(deck_configuration=[]) result = await subject.clear() assert subject.current_run_id is None diff --git a/robot-server/tests/maintenance_runs/test_run_data_manager.py b/robot-server/tests/maintenance_runs/test_run_data_manager.py index eed2138baa7..d7bfc3e03a6 100644 --- a/robot-server/tests/maintenance_runs/test_run_data_manager.py +++ b/robot-server/tests/maintenance_runs/test_run_data_manager.py @@ -91,6 +91,7 @@ async def test_create( run_id=run_id, labware_offsets=[], created_at=created_at, + deck_configuration=[], ) ).then_return(engine_state_summary) decoy.when(mock_maintenance_engine_store.current_run_created_at).then_return( @@ -100,6 +101,7 @@ async def test_create( run_id=run_id, created_at=created_at, labware_offsets=[], + deck_configuration=[], ) assert result == MaintenanceRun( @@ -138,6 +140,7 @@ async def test_create_with_options( run_id=run_id, labware_offsets=[labware_offset], created_at=created_at, + deck_configuration=[], ) ).then_return(engine_state_summary) decoy.when(mock_maintenance_engine_store.current_run_created_at).then_return( @@ -148,6 +151,7 @@ async def test_create_with_options( run_id=run_id, created_at=created_at, labware_offsets=[labware_offset], + deck_configuration=[], ) assert result == MaintenanceRun( @@ -179,6 +183,7 @@ async def test_create_engine_error( run_id, labware_offsets=[], created_at=created_at, + deck_configuration=[], ) ).then_raise(EngineConflictError("oh no")) decoy.when(mock_maintenance_engine_store.current_run_created_at).then_return( @@ -190,6 +195,7 @@ async def test_create_engine_error( run_id=run_id, created_at=created_at, labware_offsets=[], + deck_configuration=[], ) diff --git a/robot-server/tests/protocols/test_analysis_store.py b/robot-server/tests/protocols/test_analysis_store.py index 058ec0497d9..06ca93cd218 100644 --- a/robot-server/tests/protocols/test_analysis_store.py +++ b/robot-server/tests/protocols/test_analysis_store.py @@ -126,6 +126,7 @@ async def test_returned_in_order_added( subject.add_pending(protocol_id="protocol-id", analysis_id=analysis_id) await subject.update( analysis_id=analysis_id, + robot_type="OT-2 Standard", labware=[], modules=[], pipettes=[], @@ -173,6 +174,7 @@ async def test_update_adds_details_and_completes_analysis( subject.add_pending(protocol_id="protocol-id", analysis_id="analysis-id") await subject.update( analysis_id="analysis-id", + robot_type="OT-2 Standard", labware=[labware], pipettes=[pipette], # TODO(mm, 2022-10-21): Give the subject some commands, errors, and liquids here @@ -189,6 +191,7 @@ async def test_update_adds_details_and_completes_analysis( assert result == CompletedAnalysis( id="analysis-id", result=AnalysisResult.OK, + robotType="OT-2 Standard", labware=[labware], pipettes=[pipette], modules=[], @@ -201,6 +204,7 @@ async def test_update_adds_details_and_completes_analysis( "id": "analysis-id", "result": "ok", "status": "completed", + "robotType": "OT-2 Standard", "labware": [ { "id": "labware-id", @@ -270,6 +274,7 @@ async def test_update_infers_status_from_errors( subject.add_pending(protocol_id="protocol-id", analysis_id="analysis-id") await subject.update( analysis_id="analysis-id", + robot_type="OT-2 Standard", commands=commands, errors=errors, labware=[], diff --git a/robot-server/tests/protocols/test_protocol_analyzer.py b/robot-server/tests/protocols/test_protocol_analyzer.py index c655db805a5..03784e62c8e 100644 --- a/robot-server/tests/protocols/test_protocol_analyzer.py +++ b/robot-server/tests/protocols/test_protocol_analyzer.py @@ -4,6 +4,7 @@ from datetime import datetime from pathlib import Path +from opentrons_shared_data.robot.dev_types import RobotType from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons.types import MountType, DeckSlotName @@ -53,6 +54,8 @@ async def test_analyze( subject: ProtocolAnalyzer, ) -> None: """It should be able to analyze a protocol.""" + robot_type: RobotType = "OT-3 Standard" + protocol_resource = ProtocolResource( protocol_id="protocol-id", created_at=datetime(year=2021, month=1, day=1), @@ -62,7 +65,7 @@ async def test_analyze( config=JsonProtocolConfig(schema_version=123), files=[], metadata={}, - robot_type="OT-3 Standard", + robot_type=robot_type, content_hash="abc123", ), protocol_key="dummy-data-111", @@ -101,12 +104,16 @@ async def test_analyze( decoy.when( await protocol_runner.create_simulating_runner( - robot_type="OT-3 Standard", + robot_type=robot_type, protocol_config=JsonProtocolConfig(schema_version=123), ) ).then_return(json_runner) - decoy.when(await json_runner.run(protocol_resource.source)).then_return( + decoy.when( + await json_runner.run( + deck_configuration=[], protocol_source=protocol_resource.source + ) + ).then_return( protocol_runner.RunResult( commands=[analysis_command], state_summary=StateSummary( @@ -130,6 +137,7 @@ async def test_analyze( decoy.verify( await analysis_store.update( analysis_id="analysis-id", + robot_type=robot_type, commands=[analysis_command], labware=[analysis_labware], modules=[], diff --git a/robot-server/tests/robot/calibration/deck/test_user_flow.py b/robot-server/tests/robot/calibration/deck/test_user_flow.py index f162a291e75..8b6bf90c43e 100644 --- a/robot-server/tests/robot/calibration/deck/test_user_flow.py +++ b/robot-server/tests/robot/calibration/deck/test_user_flow.py @@ -1,6 +1,6 @@ import pytest import datetime -from mock import MagicMock, call +from mock import call from typing import List, Tuple from pathlib import Path from opentrons.calibration_storage import types as cal_types @@ -151,14 +151,7 @@ async def test_save_default_pick_up_current(mock_hw): def mock_update_config_item(*args): pass - uf._hw_pipette.update_config_item = MagicMock(side_effect=mock_update_config_item) - default_current = pip.pick_up_configurations.current - update_config_calls = [ - call({"pick_up_current": 0.1}), - call({"pick_up_current": default_current}), - ] await uf.pick_up_tip() - uf._hw_pipette.update_config_item.assert_has_calls(update_config_calls) async def test_return_tip(mock_user_flow): diff --git a/robot-server/tests/runs/router/conftest.py b/robot-server/tests/runs/router/conftest.py index 77d80ce2f23..f7d1f0fead6 100644 --- a/robot-server/tests/runs/router/conftest.py +++ b/robot-server/tests/runs/router/conftest.py @@ -8,6 +8,7 @@ from robot_server.runs.engine_store import EngineStore from robot_server.runs.run_data_manager import RunDataManager from robot_server.maintenance_runs import MaintenanceEngineStore +from robot_server.deck_configuration.store import DeckConfigurationStore from opentrons.protocol_engine import ProtocolEngine @@ -52,3 +53,9 @@ def mock_run_auto_deleter(decoy: Decoy) -> RunAutoDeleter: def mock_maintenance_engine_store(decoy: Decoy) -> MaintenanceEngineStore: """Get a mock MaintenanceEngineStore interface.""" return decoy.mock(cls=MaintenanceEngineStore) + + +@pytest.fixture +def mock_deck_configuration_store(decoy: Decoy) -> DeckConfigurationStore: + """Get a mock DeckConfigurationStore.""" + return decoy.mock(cls=DeckConfigurationStore) diff --git a/robot-server/tests/runs/router/test_actions_router.py b/robot-server/tests/runs/router/test_actions_router.py index 0c9dbfdb742..b6cb50d7788 100644 --- a/robot-server/tests/runs/router/test_actions_router.py +++ b/robot-server/tests/runs/router/test_actions_router.py @@ -15,6 +15,7 @@ from robot_server.runs.router.actions_router import create_run_action from robot_server.maintenance_runs import MaintenanceEngineStore +from robot_server.deck_configuration.store import DeckConfigurationStore @pytest.fixture @@ -27,6 +28,7 @@ async def test_create_run_action( decoy: Decoy, mock_run_controller: RunController, mock_maintenance_engine_store: MaintenanceEngineStore, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should create a run action.""" run_id = "some-run-id" @@ -39,12 +41,16 @@ async def test_create_run_action( createdAt=created_at, actionType=RunActionType.PLAY, ) + decoy.when( + await mock_deck_configuration_store.get_deck_configuration() + ).then_return([]) decoy.when(mock_maintenance_engine_store.current_run_id).then_return(None) decoy.when( mock_run_controller.create_action( action_id=action_id, action_type=action_type, created_at=created_at, + action_payload=[], ) ).then_return(expected_result) @@ -55,6 +61,7 @@ async def test_create_run_action( action_id=action_id, created_at=created_at, maintenance_engine_store=mock_maintenance_engine_store, + deck_configuration_store=mock_deck_configuration_store, ) assert result.content.data == expected_result @@ -65,6 +72,7 @@ async def test_play_action_clears_maintenance_run( decoy: Decoy, mock_run_controller: RunController, mock_maintenance_engine_store: MaintenanceEngineStore, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should clear an existing maintenance run before issuing play action.""" run_id = "some-run-id" @@ -77,12 +85,16 @@ async def test_play_action_clears_maintenance_run( createdAt=created_at, actionType=RunActionType.PLAY, ) + decoy.when( + await mock_deck_configuration_store.get_deck_configuration() + ).then_return([]) decoy.when(mock_maintenance_engine_store.current_run_id).then_return("some-id") decoy.when( mock_run_controller.create_action( action_id=action_id, action_type=action_type, created_at=created_at, + action_payload=[], ) ).then_return(expected_result) @@ -93,6 +105,7 @@ async def test_play_action_clears_maintenance_run( action_id=action_id, created_at=created_at, maintenance_engine_store=mock_maintenance_engine_store, + deck_configuration_store=mock_deck_configuration_store, ) decoy.verify(await mock_maintenance_engine_store.clear(), times=1) @@ -114,6 +127,7 @@ async def test_create_play_action_not_allowed( expected_error_id: str, expected_status_code: int, mock_maintenance_engine_store: MaintenanceEngineStore, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should 409 if the runner is not able to handle the action.""" run_id = "some-run-id" @@ -122,12 +136,16 @@ async def test_create_play_action_not_allowed( action_type = RunActionType.PLAY request_body = RequestModel(data=RunActionCreate(actionType=action_type)) + decoy.when( + await mock_deck_configuration_store.get_deck_configuration() + ).then_return([]) decoy.when(mock_maintenance_engine_store.current_run_id).then_return(None) decoy.when( mock_run_controller.create_action( action_id=action_id, action_type=action_type, created_at=created_at, + action_payload=[], ) ).then_raise(exception) @@ -139,6 +157,7 @@ async def test_create_play_action_not_allowed( action_id=action_id, created_at=created_at, maintenance_engine_store=mock_maintenance_engine_store, + deck_configuration_store=mock_deck_configuration_store, ) assert exc_info.value.status_code == expected_status_code diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index d52f2687be6..0abe559b843 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -39,6 +39,8 @@ update_run, ) +from robot_server.deck_configuration.store import DeckConfigurationStore + @pytest.fixture def labware_offset_create() -> LabwareOffsetCreate: @@ -55,6 +57,7 @@ async def test_create_run( mock_run_data_manager: RunDataManager, mock_run_auto_deleter: RunAutoDeleter, labware_offset_create: pe_types.LabwareOffsetCreate, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should be able to create a basic run.""" run_id = "run-id" @@ -74,12 +77,15 @@ async def test_create_run( status=pe_types.EngineStatus.IDLE, liquids=[], ) - + decoy.when( + await mock_deck_configuration_store.get_deck_configuration() + ).then_return([]) decoy.when( await mock_run_data_manager.create( run_id=run_id, created_at=run_created_at, labware_offsets=[labware_offset_create], + deck_configuration=[], protocol=None, ) ).then_return(expected_response) @@ -92,6 +98,7 @@ async def test_create_run( run_id=run_id, created_at=run_created_at, run_auto_deleter=mock_run_auto_deleter, + deck_configuration_store=mock_deck_configuration_store, ) assert result.content.data == expected_response @@ -105,6 +112,7 @@ async def test_create_protocol_run( mock_protocol_store: ProtocolStore, mock_run_data_manager: RunDataManager, mock_run_auto_deleter: RunAutoDeleter, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should be able to create a protocol run.""" run_id = "run-id" @@ -140,7 +148,9 @@ async def test_create_protocol_run( status=pe_types.EngineStatus.IDLE, liquids=[], ) - + decoy.when( + await mock_deck_configuration_store.get_deck_configuration() + ).then_return([]) decoy.when(mock_protocol_store.get(protocol_id=protocol_id)).then_return( protocol_resource ) @@ -150,6 +160,7 @@ async def test_create_protocol_run( run_id=run_id, created_at=run_created_at, labware_offsets=[], + deck_configuration=[], protocol=protocol_resource, ) ).then_return(expected_response) @@ -161,6 +172,7 @@ async def test_create_protocol_run( run_id=run_id, created_at=run_created_at, run_auto_deleter=mock_run_auto_deleter, + deck_configuration_store=mock_deck_configuration_store, ) assert result.content.data == expected_response @@ -172,16 +184,20 @@ async def test_create_protocol_run( async def test_create_protocol_run_bad_protocol_id( decoy: Decoy, mock_protocol_store: ProtocolStore, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should 404 if a protocol for a run does not exist.""" error = ProtocolNotFoundError("protocol-id") - + decoy.when( + await mock_deck_configuration_store.get_deck_configuration() + ).then_return([]) decoy.when(mock_protocol_store.get(protocol_id="protocol-id")).then_raise(error) with pytest.raises(ApiError) as exc_info: await create_run( request_body=RequestModel(data=RunCreate(protocolId="protocol-id")), protocol_store=mock_protocol_store, + deck_configuration_store=mock_deck_configuration_store, ) assert exc_info.value.status_code == 404 @@ -192,15 +208,20 @@ async def test_create_run_conflict( decoy: Decoy, mock_run_data_manager: RunDataManager, mock_run_auto_deleter: RunAutoDeleter, + mock_deck_configuration_store: DeckConfigurationStore, ) -> None: """It should respond with a conflict error if multiple engines are created.""" created_at = datetime(year=2021, month=1, day=1) + decoy.when( + await mock_deck_configuration_store.get_deck_configuration() + ).then_return([]) decoy.when( await mock_run_data_manager.create( run_id="run-id", created_at=created_at, labware_offsets=[], + deck_configuration=[], protocol=None, ) ).then_raise(EngineConflictError("oh no")) @@ -212,6 +233,7 @@ async def test_create_run_conflict( request_body=None, run_data_manager=mock_run_data_manager, run_auto_deleter=mock_run_auto_deleter, + deck_configuration_store=mock_deck_configuration_store, ) assert exc_info.value.status_code == 409 diff --git a/robot-server/tests/runs/test_engine_store.py b/robot-server/tests/runs/test_engine_store.py index fc22e4ea900..bd8ef9b3678 100644 --- a/robot-server/tests/runs/test_engine_store.py +++ b/robot-server/tests/runs/test_engine_store.py @@ -50,7 +50,9 @@ async def json_protocol_source(tmp_path: Path) -> ProtocolSource: async def test_create_engine(subject: EngineStore) -> None: """It should create an engine for a run.""" - result = await subject.create(run_id="run-id", labware_offsets=[], protocol=None) + result = await subject.create( + run_id="run-id", labware_offsets=[], protocol=None, deck_configuration=[] + ) assert subject.current_run_id == "run-id" assert isinstance(result, StateSummary) @@ -78,6 +80,7 @@ async def test_create_engine_with_protocol( result = await subject.create( run_id="run-id", labware_offsets=[], + deck_configuration=[], protocol=protocol, ) assert subject.current_run_id == "run-id" @@ -99,7 +102,9 @@ async def test_create_engine_uses_robot_type( hardware_api=hardware_api, robot_type=robot_type, deck_type=deck_type ) - await subject.create(run_id="run-id", labware_offsets=[], protocol=None) + await subject.create( + run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + ) assert subject.engine.state_view.config.robot_type == robot_type @@ -115,6 +120,7 @@ async def test_create_engine_with_labware_offsets(subject: EngineStore) -> None: result = await subject.create( run_id="run-id", labware_offsets=[labware_offset], + deck_configuration=[], protocol=None, ) @@ -131,18 +137,24 @@ async def test_create_engine_with_labware_offsets(subject: EngineStore) -> None: async def test_archives_state_if_engine_already_exists(subject: EngineStore) -> None: """It should not create more than one engine / runner pair.""" - await subject.create(run_id="run-id-1", labware_offsets=[], protocol=None) + await subject.create( + run_id="run-id-1", labware_offsets=[], deck_configuration=[], protocol=None + ) with pytest.raises(EngineConflictError): - await subject.create(run_id="run-id-2", labware_offsets=[], protocol=None) + await subject.create( + run_id="run-id-2", labware_offsets=[], deck_configuration=[], protocol=None + ) assert subject.current_run_id == "run-id-1" async def test_clear_engine(subject: EngineStore) -> None: """It should clear a stored engine entry.""" - await subject.create(run_id="run-id", labware_offsets=[], protocol=None) - await subject.runner.run() + await subject.create( + run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + ) + await subject.runner.run(deck_configuration=[]) result = await subject.clear() assert subject.current_run_id is None @@ -159,8 +171,10 @@ async def test_clear_engine_not_stopped_or_idle( subject: EngineStore, json_protocol_source: ProtocolSource ) -> None: """It should raise a conflict if the engine is not stopped.""" - await subject.create(run_id="run-id", labware_offsets=[], protocol=None) - subject.runner.play() + await subject.create( + run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + ) + subject.runner.play(deck_configuration=[]) with pytest.raises(EngineConflictError): await subject.clear() @@ -168,7 +182,9 @@ async def test_clear_engine_not_stopped_or_idle( async def test_clear_idle_engine(subject: EngineStore) -> None: """It should successfully clear engine if idle (not started).""" - await subject.create(run_id="run-id", labware_offsets=[], protocol=None) + await subject.create( + run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + ) assert subject.engine is not None assert subject.runner is not None @@ -210,7 +226,9 @@ async def test_get_default_engine_robot_type( async def test_get_default_engine_current_unstarted(subject: EngineStore) -> None: """It should allow a default engine if another engine current but unstarted.""" - await subject.create(run_id="run-id", labware_offsets=[], protocol=None) + await subject.create( + run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + ) result = await subject.get_default_engine() assert isinstance(result, ProtocolEngine) @@ -218,7 +236,9 @@ async def test_get_default_engine_current_unstarted(subject: EngineStore) -> Non async def test_get_default_engine_conflict(subject: EngineStore) -> None: """It should not allow a default engine if another engine is executing commands.""" - await subject.create(run_id="run-id", labware_offsets=[], protocol=None) + await subject.create( + run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + ) subject.engine.play() with pytest.raises(EngineConflictError): @@ -227,7 +247,9 @@ async def test_get_default_engine_conflict(subject: EngineStore) -> None: async def test_get_default_engine_run_stopped(subject: EngineStore) -> None: """It allow a default engine if another engine is terminal.""" - await subject.create(run_id="run-id", labware_offsets=[], protocol=None) + await subject.create( + run_id="run-id", labware_offsets=[], deck_configuration=[], protocol=None + ) await subject.engine.finish() result = await subject.get_default_engine() diff --git a/robot-server/tests/runs/test_run_controller.py b/robot-server/tests/runs/test_run_controller.py index 7387af2d912..da433043650 100644 --- a/robot-server/tests/runs/test_run_controller.py +++ b/robot-server/tests/runs/test_run_controller.py @@ -102,6 +102,7 @@ async def test_create_play_action_to_resume( action_id="some-action-id", action_type=RunActionType.PLAY, created_at=datetime(year=2021, month=1, day=1), + action_payload=[], ) assert result == RunAction( @@ -112,7 +113,7 @@ async def test_create_play_action_to_resume( decoy.verify(mock_run_store.insert_action(run_id, result), times=1) decoy.verify(mock_json_runner.play(), times=1) - decoy.verify(await mock_json_runner.run(), times=0) + decoy.verify(await mock_json_runner.run(deck_configuration=[]), times=0) async def test_create_play_action_to_start( @@ -134,6 +135,7 @@ async def test_create_play_action_to_start( action_id="some-action-id", action_type=RunActionType.PLAY, created_at=datetime(year=2021, month=1, day=1), + action_payload=[], ) assert result == RunAction( @@ -145,16 +147,16 @@ async def test_create_play_action_to_start( decoy.verify(mock_run_store.insert_action(run_id, result), times=1) background_task_captor = matchers.Captor() - decoy.verify(mock_task_runner.run(background_task_captor)) + decoy.verify(mock_task_runner.run(background_task_captor, deck_configuration=[])) - decoy.when(await mock_python_runner.run()).then_return( + decoy.when(await mock_python_runner.run(deck_configuration=[])).then_return( RunResult( commands=protocol_commands, state_summary=engine_state_summary, ) ) - await background_task_captor.value() + await background_task_captor.value(deck_configuration=[]) decoy.verify( mock_run_store.update_run_state( @@ -178,6 +180,7 @@ async def test_create_pause_action( action_id="some-action-id", action_type=RunActionType.PAUSE, created_at=datetime(year=2021, month=1, day=1), + action_payload=[], ) assert result == RunAction( @@ -203,6 +206,7 @@ async def test_create_stop_action( action_id="some-action-id", action_type=RunActionType.STOP, created_at=datetime(year=2021, month=1, day=1), + action_payload=[], ) assert result == RunAction( @@ -243,4 +247,5 @@ async def test_action_not_allowed( action_id="whatever", action_type=action_type, created_at=datetime(year=2021, month=1, day=1), + action_payload=[], ) diff --git a/robot-server/tests/runs/test_run_data_manager.py b/robot-server/tests/runs/test_run_data_manager.py index 21e7cb8072b..33e348d8854 100644 --- a/robot-server/tests/runs/test_run_data_manager.py +++ b/robot-server/tests/runs/test_run_data_manager.py @@ -121,7 +121,12 @@ async def test_create( created_at = datetime(year=2021, month=1, day=1) decoy.when( - await mock_engine_store.create(run_id=run_id, labware_offsets=[], protocol=None) + await mock_engine_store.create( + run_id=run_id, + labware_offsets=[], + protocol=None, + deck_configuration=[], + ) ).then_return(engine_state_summary) decoy.when( mock_run_store.insert( @@ -136,6 +141,7 @@ async def test_create( created_at=created_at, labware_offsets=[], protocol=None, + deck_configuration=[], ) assert result == Run( @@ -184,6 +190,7 @@ async def test_create_with_options( run_id=run_id, labware_offsets=[labware_offset], protocol=protocol, + deck_configuration=[], ) ).then_return(engine_state_summary) @@ -200,6 +207,7 @@ async def test_create_with_options( created_at=created_at, labware_offsets=[labware_offset], protocol=protocol, + deck_configuration=[], ) assert result == Run( @@ -229,7 +237,12 @@ async def test_create_engine_error( created_at = datetime(year=2021, month=1, day=1) decoy.when( - await mock_engine_store.create(run_id, labware_offsets=[], protocol=None) + await mock_engine_store.create( + run_id, + labware_offsets=[], + protocol=None, + deck_configuration=[], + ) ).then_raise(EngineConflictError("oh no")) with pytest.raises(EngineConflictError): @@ -238,6 +251,7 @@ async def test_create_engine_error( created_at=created_at, labware_offsets=[], protocol=None, + deck_configuration=[], ) decoy.verify( @@ -602,6 +616,7 @@ async def test_create_archives_existing( run_id=run_id_new, labware_offsets=[], protocol=None, + deck_configuration=[], ) ).then_return(engine_state_summary) @@ -618,6 +633,7 @@ async def test_create_archives_existing( created_at=datetime(year=2021, month=1, day=1), labware_offsets=[], protocol=None, + deck_configuration=[], ) decoy.verify( diff --git a/robot-server/tests/runs/test_run_store.py b/robot-server/tests/runs/test_run_store.py index 3350a290d13..c805b944714 100644 --- a/robot-server/tests/runs/test_run_store.py +++ b/robot-server/tests/runs/test_run_store.py @@ -104,6 +104,46 @@ def state_summary() -> StateSummary: ) +@pytest.fixture +def invalid_state_summary() -> StateSummary: + """Should fail pydantic validation.""" + analysis_error = pe_errors.ErrorOccurrence.construct( + id="error-id", + # Invalid value here should fail analysis + createdAt=MountType.LEFT, # type: ignore + errorType="BadError", + detail="oh no", + ) + + analysis_labware = pe_types.LoadedLabware( + id="labware-id", + loadName="load-name", + definitionUri="namespace/load-name/42", + location=pe_types.DeckSlotLocation(slotName=DeckSlotName.SLOT_1), + offsetId=None, + ) + + analysis_pipette = pe_types.LoadedPipette( + id="pipette-id", + pipetteName=PipetteNameType.P300_SINGLE, + mount=MountType.LEFT, + ) + + liquids = [Liquid(id="some-id", displayName="water", description="water desc")] + + return StateSummary( + errors=[analysis_error], + labware=[analysis_labware], + pipettes=[analysis_pipette], + # TODO(mc, 2022-02-14): evaluate usage of modules in the analysis resp. + modules=[], + # TODO (tz 22-4-19): added the field to class. make sure what to initialize + labwareOffsets=[], + status=EngineStatus.IDLE, + liquids=liquids, + ) + + def test_update_run_state( subject: RunStore, state_summary: StateSummary, @@ -367,6 +407,22 @@ def test_get_state_summary(subject: RunStore, state_summary: StateSummary) -> No assert result == state_summary +def test_get_state_summary_failure( + subject: RunStore, invalid_state_summary: StateSummary +) -> None: + """It should return None.""" + subject.insert( + run_id="run-id", + protocol_id=None, + created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), + ) + subject.update_run_state( + run_id="run-id", summary=invalid_state_summary, commands=[] + ) + result = subject.get_state_summary(run_id="run-id") + assert result is None + + def test_get_state_summary_none(subject: RunStore) -> None: """It should return None if no state data stored.""" subject.insert( diff --git a/shared-data/command/schemas/8.json b/shared-data/command/schemas/8.json index 81f1d37ca5f..b44f044c02e 100644 --- a/shared-data/command/schemas/8.json +++ b/shared-data/command/schemas/8.json @@ -14,6 +14,9 @@ { "$ref": "#/definitions/ConfigureForVolumeCreate" }, + { + "$ref": "#/definitions/ConfigureNozzleLayoutCreate" + }, { "$ref": "#/definitions/CustomCreate" }, @@ -65,6 +68,9 @@ { "$ref": "#/definitions/MoveToWellCreate" }, + { + "$ref": "#/definitions/MoveToAddressableAreaCreate" + }, { "$ref": "#/definitions/PrepareToAspirateCreate" }, @@ -439,6 +445,168 @@ }, "required": ["params"] }, + "EmptyNozzleLayoutConfiguration": { + "title": "EmptyNozzleLayoutConfiguration", + "description": "Empty basemodel to represent a reset to the nozzle configuration. Sending no parameters resets to default.", + "type": "object", + "properties": { + "style": { + "title": "Style", + "default": "EMPTY", + "enum": ["EMPTY"], + "type": "string" + } + } + }, + "SingleNozzleLayoutConfiguration": { + "title": "SingleNozzleLayoutConfiguration", + "description": "Minimum information required for a new nozzle configuration.", + "type": "object", + "properties": { + "style": { + "title": "Style", + "default": "SINGLE", + "enum": ["SINGLE"], + "type": "string" + }, + "primary_nozzle": { + "title": "Primary Nozzle", + "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + "enum": ["A1", "H1", "A12", "H12"], + "type": "string" + } + }, + "required": ["primary_nozzle"] + }, + "RowNozzleLayoutConfiguration": { + "title": "RowNozzleLayoutConfiguration", + "description": "Minimum information required for a new nozzle configuration.", + "type": "object", + "properties": { + "style": { + "title": "Style", + "default": "ROW", + "enum": ["ROW"], + "type": "string" + }, + "primary_nozzle": { + "title": "Primary Nozzle", + "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + "enum": ["A1", "H1", "A12", "H12"], + "type": "string" + } + }, + "required": ["primary_nozzle"] + }, + "ColumnNozzleLayoutConfiguration": { + "title": "ColumnNozzleLayoutConfiguration", + "description": "Information required for nozzle configurations of type ROW and COLUMN.", + "type": "object", + "properties": { + "style": { + "title": "Style", + "default": "COLUMN", + "enum": ["COLUMN"], + "type": "string" + }, + "primary_nozzle": { + "title": "Primary Nozzle", + "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + "enum": ["A1", "H1", "A12", "H12"], + "type": "string" + } + }, + "required": ["primary_nozzle"] + }, + "QuadrantNozzleLayoutConfiguration": { + "title": "QuadrantNozzleLayoutConfiguration", + "description": "Information required for nozzle configurations of type QUADRANT.", + "type": "object", + "properties": { + "style": { + "title": "Style", + "default": "QUADRANT", + "enum": ["QUADRANT"], + "type": "string" + }, + "primary_nozzle": { + "title": "Primary Nozzle", + "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + "enum": ["A1", "H1", "A12", "H12"], + "type": "string" + }, + "front_right_nozzle": { + "title": "Front Right Nozzle", + "description": "The front right nozzle in your configuration.", + "pattern": "[A-Z][0-100]", + "type": "string" + } + }, + "required": ["primary_nozzle", "front_right_nozzle"] + }, + "ConfigureNozzleLayoutParams": { + "title": "ConfigureNozzleLayoutParams", + "description": "Parameters required to configure the nozzle layout for a specific pipette.", + "type": "object", + "properties": { + "pipetteId": { + "title": "Pipetteid", + "description": "Identifier of pipette to use for liquid handling.", + "type": "string" + }, + "configuration_params": { + "title": "Configuration Params", + "anyOf": [ + { + "$ref": "#/definitions/EmptyNozzleLayoutConfiguration" + }, + { + "$ref": "#/definitions/SingleNozzleLayoutConfiguration" + }, + { + "$ref": "#/definitions/RowNozzleLayoutConfiguration" + }, + { + "$ref": "#/definitions/ColumnNozzleLayoutConfiguration" + }, + { + "$ref": "#/definitions/QuadrantNozzleLayoutConfiguration" + } + ] + } + }, + "required": ["pipetteId", "configuration_params"] + }, + "ConfigureNozzleLayoutCreate": { + "title": "ConfigureNozzleLayoutCreate", + "description": "Configure nozzle layout creation request model.", + "type": "object", + "properties": { + "commandType": { + "title": "Commandtype", + "default": "configureNozzleLayout", + "enum": ["configureNozzleLayout"], + "type": "string" + }, + "params": { + "$ref": "#/definitions/ConfigureNozzleLayoutParams" + }, + "intent": { + "description": "The reason the command was added. If not specified or `protocol`, the command will be treated as part of the protocol run itself, and added to the end of the existing command queue.\n\nIf `setup`, the command will be treated as part of run setup. A setup command may only be enqueued if the run has not started.\n\nUse setup commands for activities like pre-run calibration checks and module setup, like pre-heating.", + "allOf": [ + { + "$ref": "#/definitions/CommandIntent" + } + ] + }, + "key": { + "title": "Key", + "description": "A key value, unique in this run, that can be used to track the same logical command across multiple runs of the same protocol. If a value is not provided, one will be generated.", + "type": "string" + } + }, + "required": ["params"] + }, "CustomParams": { "title": "CustomParams", "description": "Payload used by a custom command.", @@ -1067,6 +1235,19 @@ }, "required": ["labwareId"] }, + "AddressableAreaLocation": { + "title": "AddressableAreaLocation", + "description": "The location of something place in an addressable area. This is a superset of deck slots.", + "type": "object", + "properties": { + "addressableAreaName": { + "title": "Addressableareaname", + "description": "The name of the addressable area that you want to use. Valid values are the `id`s of `addressableArea`s in the [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck).", + "type": "string" + } + }, + "required": ["addressableAreaName"] + }, "LoadLabwareParams": { "title": "LoadLabwareParams", "description": "Payload required to load a labware into a slot.", @@ -1088,6 +1269,9 @@ { "enum": ["offDeck"], "type": "string" + }, + { + "$ref": "#/definitions/AddressableAreaLocation" } ] }, @@ -1415,6 +1599,9 @@ { "enum": ["offDeck"], "type": "string" + }, + { + "$ref": "#/definitions/AddressableAreaLocation" } ] }, @@ -1705,6 +1892,70 @@ }, "required": ["params"] }, + "MoveToAddressableAreaParams": { + "title": "MoveToAddressableAreaParams", + "description": "Payload required to move a pipette to a specific addressable area.", + "type": "object", + "properties": { + "minimumZHeight": { + "title": "Minimumzheight", + "description": "Optional minimal Z margin in mm. If this is larger than the API's default safe Z margin, it will make the arc higher. If it's smaller, it will have no effect.", + "type": "number" + }, + "forceDirect": { + "title": "Forcedirect", + "description": "If true, moving from one labware/well to another will not arc to the default safe z, but instead will move directly to the specified location. This will also force the `minimumZHeight` param to be ignored. A 'direct' movement is in X/Y/Z simultaneously.", + "default": false, + "type": "boolean" + }, + "speed": { + "title": "Speed", + "description": "Override the travel speed in mm/s. This controls the straight linear speed of motion.", + "type": "number" + }, + "pipetteId": { + "title": "Pipetteid", + "description": "Identifier of pipette to use for liquid handling.", + "type": "string" + }, + "addressableAreaName": { + "title": "Addressableareaname", + "description": "The name of the addressable area that you want to use. Valid values are the `id`s of `addressableArea`s in the [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck).", + "type": "string" + } + }, + "required": ["pipetteId", "addressableAreaName"] + }, + "MoveToAddressableAreaCreate": { + "title": "MoveToAddressableAreaCreate", + "description": "Move to addressable area command creation request model.", + "type": "object", + "properties": { + "commandType": { + "title": "Commandtype", + "default": "moveToAddressableArea", + "enum": ["moveToAddressableArea"], + "type": "string" + }, + "params": { + "$ref": "#/definitions/MoveToAddressableAreaParams" + }, + "intent": { + "description": "The reason the command was added. If not specified or `protocol`, the command will be treated as part of the protocol run itself, and added to the end of the existing command queue.\n\nIf `setup`, the command will be treated as part of run setup. A setup command may only be enqueued if the run has not started.\n\nUse setup commands for activities like pre-run calibration checks and module setup, like pre-heating.", + "allOf": [ + { + "$ref": "#/definitions/CommandIntent" + } + ] + }, + "key": { + "title": "Key", + "description": "A key value, unique in this run, that can be used to track the same logical command across multiple runs of the same protocol. If a value is not provided, one will be generated.", + "type": "string" + } + }, + "required": ["params"] + }, "PrepareToAspirateParams": { "title": "PrepareToAspirateParams", "description": "Parameters required to prepare a specific pipette for aspiration.", diff --git a/shared-data/command/types/pipetting.ts b/shared-data/command/types/pipetting.ts index fe54ed1cf81..98cfad69f99 100644 --- a/shared-data/command/types/pipetting.ts +++ b/shared-data/command/types/pipetting.ts @@ -1,6 +1,7 @@ import type { CommonCommandRunTimeInfo, CommonCommandCreateInfo } from '.' export type PipettingRunTimeCommand = | AspirateRunTimeCommand + | AspirateInPlaceRunTimeCommand | BlowoutInPlaceRunTimeCommand | BlowoutRunTimeCommand | ConfigureForVolumeRunTimeCommand @@ -14,6 +15,7 @@ export type PipettingRunTimeCommand = export type PipettingCreateCommand = | AspirateCreateCommand + | AspirateInPlaceCreateCommand | BlowoutCreateCommand | BlowoutInPlaceCreateCommand | ConfigureForVolumeCreateCommand @@ -50,6 +52,16 @@ export interface AspirateRunTimeCommand result?: BasicLiquidHandlingResult } +export interface AspirateInPlaceCreateCommand extends CommonCommandCreateInfo { + commandType: 'aspirateInPlace' + params: AspirateInPlaceParams +} +export interface AspirateInPlaceRunTimeCommand + extends CommonCommandRunTimeInfo, + AspirateInPlaceCreateCommand { + result?: BasicLiquidHandlingResult +} + export type DispenseParams = AspDispAirgapParams & { pushOut?: number } export interface DispenseCreateCommand extends CommonCommandCreateInfo { commandType: 'dispense' @@ -174,6 +186,11 @@ export interface DispenseInPlaceParams { pushOut?: number } +export interface AspirateInPlaceParams { + pipetteId: string + volume: number + flowRate: number // µL/s +} interface FlowRateParams { flowRate: number // µL/s } diff --git a/shared-data/command/types/setup.ts b/shared-data/command/types/setup.ts index fd43093d16c..2fd53972e7b 100644 --- a/shared-data/command/types/setup.ts +++ b/shared-data/command/types/setup.ts @@ -5,7 +5,6 @@ import type { LabwareOffset, PipetteName, ModuleModel, - FixtureLoadName, Cutout, } from '../../js' @@ -71,7 +70,20 @@ export interface LoadFixtureRunTimeCommand result?: LoadLabwareResult } +export interface ConfigureNozzleLayoutCreateCommand + extends CommonCommandCreateInfo { + commandType: 'configureNozzleLayout' + params: ConfigureNozzleLayoutParams +} + +export interface ConfigureNozzleLayoutRunTimeCommand + extends CommonCommandRunTimeInfo, + ConfigureNozzleLayoutCreateCommand { + result?: {} +} + export type SetupRunTimeCommand = + | ConfigureNozzleLayoutRunTimeCommand | LoadPipetteRunTimeCommand | LoadLabwareRunTimeCommand | LoadFixtureRunTimeCommand @@ -80,6 +92,7 @@ export type SetupRunTimeCommand = | MoveLabwareRunTimeCommand export type SetupCreateCommand = + | ConfigureNozzleLayoutCreateCommand | LoadPipetteCreateCommand | LoadLabwareCreateCommand | LoadFixtureCreateCommand @@ -92,11 +105,13 @@ export type LabwareLocation = | { slotName: string } | { moduleId: string } | { labwareId: string } + | { addressableAreaName: string } export type NonStackedLocation = | 'offDeck' | { slotName: string } | { moduleId: string } + | { addressableAreaName: string } export interface ModuleLocation { slotName: string @@ -156,6 +171,29 @@ interface LoadLiquidResult { interface LoadFixtureParams { location: { cutout: Cutout } - loadName: FixtureLoadName + loadName: string fixtureId?: string } + +const COLUMN = 'COLUMN' +const SINGLE = 'SINGLE' +const ROW = 'ROW' +const QUADRANT = 'QUADRANT' +const EMPTY = 'EMPTY' + +export type NozzleConfigurationStyle = + | typeof COLUMN + | typeof SINGLE + | typeof ROW + | typeof QUADRANT + | typeof EMPTY + +interface NozzleConfigurationParams { + primary_nozzle: string + style: NozzleConfigurationStyle +} + +interface ConfigureNozzleLayoutParams { + pipetteId: string + configuration_params: NozzleConfigurationParams +} diff --git a/shared-data/deck/definitions/3/ot3_standard.json b/shared-data/deck/definitions/3/ot3_standard.json index 775ba50960f..bead3586dba 100644 --- a/shared-data/deck/definitions/3/ot3_standard.json +++ b/shared-data/deck/definitions/3/ot3_standard.json @@ -982,7 +982,7 @@ "dropOffset": { "x": 0, "y": 0, - "z": -0.25 + "z": -0.75 } } } diff --git a/shared-data/deck/definitions/4/ot2_short_trash.json b/shared-data/deck/definitions/4/ot2_short_trash.json index 64ebd34a511..7dfb7cfc1aa 100644 --- a/shared-data/deck/definitions/4/ot2_short_trash.json +++ b/shared-data/deck/definitions/4/ot2_short_trash.json @@ -200,6 +200,18 @@ "heaterShakerModuleType" ] }, + { + "id": "12", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 12", + "compatibleModuleTypes": [] + }, { "id": "shortFixedTrash", "areaType": "fixedTrash", @@ -375,7 +387,8 @@ "cutout8": ["8"], "cutout9": ["9"], "cutout10": ["10"], - "cutout11": ["11"] + "cutout11": ["11"], + "cutout12": ["12"] } }, { diff --git a/shared-data/deck/definitions/4/ot2_standard.json b/shared-data/deck/definitions/4/ot2_standard.json index e28257ca332..eb6d446f69a 100644 --- a/shared-data/deck/definitions/4/ot2_standard.json +++ b/shared-data/deck/definitions/4/ot2_standard.json @@ -200,6 +200,18 @@ "heaterShakerModuleType" ] }, + { + "id": "12", + "areaType": "slot", + "offsetFromCutoutFixture": [0.0, 0.0, 0.0], + "boundingBox": { + "xDimension": 128.0, + "yDimension": 86.0, + "zDimension": 0 + }, + "displayName": "Slot 12", + "compatibleModuleTypes": [] + }, { "id": "fixedTrash", "areaType": "fixedTrash", @@ -375,7 +387,8 @@ "cutout8": ["8"], "cutout9": ["9"], "cutout10": ["10"], - "cutout11": ["11"] + "cutout11": ["11"], + "cutout12": ["12"] } }, { diff --git a/shared-data/deck/definitions/4/ot3_standard.json b/shared-data/deck/definitions/4/ot3_standard.json index 333fd6d59c3..5d5441a8fcf 100644 --- a/shared-data/deck/definitions/4/ot3_standard.json +++ b/shared-data/deck/definitions/4/ot3_standard.json @@ -205,6 +205,7 @@ "id": "A4", "areaType": "slot", "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -217,6 +218,7 @@ "id": "B4", "areaType": "slot", "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -229,6 +231,7 @@ "id": "C4", "areaType": "slot", "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -241,6 +244,7 @@ "id": "D4", "areaType": "slot", "offsetFromCutoutFixture": [164.0, 0.0, 14.5], + "matingSurfaceUnitVector": [-1, 1, -1], "boundingBox": { "xDimension": 128.0, "yDimension": 86.0, @@ -250,7 +254,98 @@ "compatibleModuleTypes": [] }, { - "id": "movableTrash", + "id": "movableTrashD1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-101.5, -2.75, 0.0], + "boundingBox": { + "xDimension": 246.5, + "yDimension": 91.5, + "zDimension": 40 + }, + "displayName": "Trash Bin", + "ableToDropTips": true, + "dropTipsOffset": [123.25, 45.75, 40.0] + }, + { + "id": "movableTrashC1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-101.5, -2.75, 0.0], + "boundingBox": { + "xDimension": 246.5, + "yDimension": 91.5, + "zDimension": 40 + }, + "displayName": "Trash Bin", + "ableToDropTips": true, + "dropTipsOffset": [123.25, 45.75, 40.0] + }, + { + "id": "movableTrashB1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-101.5, -2.75, 0.0], + "boundingBox": { + "xDimension": 246.5, + "yDimension": 91.5, + "zDimension": 40 + }, + "displayName": "Trash Bin", + "ableToDropTips": true, + "dropTipsOffset": [123.25, 45.75, 40.0] + }, + { + "id": "movableTrashA1", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-101.5, -2.75, 0.0], + "boundingBox": { + "xDimension": 246.5, + "yDimension": 91.5, + "zDimension": 40 + }, + "displayName": "Trash Bin", + "ableToDropTips": true, + "dropTipsOffset": [123.25, 45.75, 40.0] + }, + { + "id": "movableTrashD3", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "boundingBox": { + "xDimension": 246.5, + "yDimension": 91.5, + "zDimension": 40 + }, + "displayName": "Trash Bin", + "ableToDropTips": true, + "dropTipsOffset": [123.25, 45.75, 40.0] + }, + { + "id": "movableTrashC3", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "boundingBox": { + "xDimension": 246.5, + "yDimension": 91.5, + "zDimension": 40 + }, + "displayName": "Trash Bin", + "ableToDropTips": true, + "dropTipsOffset": [123.25, 45.75, 40.0] + }, + { + "id": "movableTrashB3", + "areaType": "movableTrash", + "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], + "boundingBox": { + "xDimension": 246.5, + "yDimension": 91.5, + "zDimension": 40 + }, + "displayName": "Trash Bin", + "ableToDropTips": true, + "dropTipsOffset": [123.25, 45.75, 40.0] + }, + { + "id": "movableTrashA3", "areaType": "movableTrash", "offsetFromCutoutFixture": [-17.0, -2.75, 0.0], "boundingBox": { @@ -433,14 +528,14 @@ ], "displayName": "Slot With Movable Trash", "providesAddressableAreas": { - "cutoutD1": ["movableTrash"], - "cutoutC1": ["movableTrash"], - "cutoutB1": ["movableTrash"], - "cutoutA1": ["movableTrash"], - "cutoutD3": ["movableTrash"], - "cutoutC3": ["movableTrash"], - "cutoutB3": ["movableTrash"], - "cutoutA3": ["movableTrash"] + "cutoutD1": ["movableTrashD1"], + "cutoutC1": ["movableTrashC1"], + "cutoutB1": ["movableTrashB1"], + "cutoutA1": ["movableTrashA1"], + "cutoutD3": ["movableTrashD3"], + "cutoutC3": ["movableTrashC3"], + "cutoutB3": ["movableTrashB3"], + "cutoutA3": ["movableTrashA3"] } }, { @@ -495,7 +590,7 @@ "dropOffset": { "x": 0, "y": 0, - "z": -0.25 + "z": -0.75 } } } diff --git a/shared-data/deck/index.ts b/shared-data/deck/index.ts new file mode 100644 index 00000000000..f05ccb33b7f --- /dev/null +++ b/shared-data/deck/index.ts @@ -0,0 +1 @@ +export * from './types/schemaV4' diff --git a/shared-data/deck/types/schemaV4.ts b/shared-data/deck/types/schemaV4.ts new file mode 100644 index 00000000000..9f4d6045fc4 --- /dev/null +++ b/shared-data/deck/types/schemaV4.ts @@ -0,0 +1,82 @@ +export type FlexAddressableAreaName = + | 'D1' + | 'D2' + | 'D3' + | 'C1' + | 'C2' + | 'C3' + | 'B1' + | 'B2' + | 'B3' + | 'A1' + | 'A2' + | 'A3' + | 'A4' + | 'B4' + | 'C4' + | 'D4' + | 'movableTrashA1' + | 'movableTrashA3' + | 'movableTrashB1' + | 'movableTrashB3' + | 'movableTrashC1' + | 'movableTrashC3' + | 'movableTrashD1' + | 'movableTrashD3' + | '1and8ChannelWasteChute' + | '96ChannelWasteChute' + | 'gripperWasteChute' + +export type OT2AddressableAreaName = + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | '8' + | '9' + | '10' + | '11' + | 'fixedTrash' + +export type AddressableAreaName = + | FlexAddressableAreaName + | OT2AddressableAreaName + +export type CutoutId = + | 'cutoutD1' + | 'cutoutD2' + | 'cutoutD3' + | 'cutoutC1' + | 'cutoutC2' + | 'cutoutC3' + | 'cutoutB1' + | 'cutoutB2' + | 'cutoutB3' + | 'cutoutA1' + | 'cutoutA2' + | 'cutoutA3' + +export type SingleSlotCutoutFixtureId = + | 'singleLeftSlot' + | 'singleCenterSlot' + | 'singleRightSlot' + +export type StagingAreaRightSlotFixtureId = 'stagingAreaRightSlot' + +export type TrashBinAdapterCutoutFixtureId = 'trashBinAdapter' + +export type WasteChuteCutoutFixtureId = + | 'wasteChuteRightAdapterCovered' + | 'wasteChuteRightAdapterNoCover' + | 'stagingAreaSlotWithWasteChuteRightAdapterCovered' + | 'stagingAreaSlotWithWasteChuteRightAdapterNoCover' + +export type CutoutFixtureId = + | SingleSlotCutoutFixtureId + | StagingAreaRightSlotFixtureId + | TrashBinAdapterCutoutFixtureId + | WasteChuteCutoutFixtureId + | 'stagingAreaRightSlot' diff --git a/shared-data/errors/definitions/1/errors.json b/shared-data/errors/definitions/1/errors.json index 28caa98cb7a..1f31ac9af6e 100644 --- a/shared-data/errors/definitions/1/errors.json +++ b/shared-data/errors/definitions/1/errors.json @@ -213,6 +213,10 @@ "4006": { "detail": "Invalid Protocol Data", "category": "generalError" + }, + "4007": { + "detail": "API Command is misconfigured", + "category": "generalError" } } } diff --git a/shared-data/js/__tests__/pipetteSchemaV2.test.ts b/shared-data/js/__tests__/pipetteSchemaV2.test.ts index ae85233ab8c..1dce5ef754b 100644 --- a/shared-data/js/__tests__/pipetteSchemaV2.test.ts +++ b/shared-data/js/__tests__/pipetteSchemaV2.test.ts @@ -13,12 +13,12 @@ const allGeometryDefinitions = path.join( const allGeneralDefinitions = path.join( __dirname, - '../../labware/definitions/2/general/**/**/*.json' + '../../pipette/definitions/2/general/**/**/*.json' ) const allLiquidDefinitions = path.join( __dirname, - '../../labware/definitions/2/liquid/**/**/*.json' + '../../pipette/definitions/2/liquid/**/**/*.json' ) const ajv = new Ajv({ allErrors: true, jsonPointers: true }) @@ -29,11 +29,8 @@ const validateGeneralSpecs = ajv.compile(generalSpecsSchema) describe('test schema against all liquid specs definitions', () => { const liquidPaths = glob.sync(allLiquidDefinitions) - - beforeAll(() => { - // Make sure definitions path didn't break, which would give you false positives - expect(liquidPaths.length).toBeGreaterThan(0) - }) + // Make sure definitions path didn't break, which would give you false positives + expect(liquidPaths.length).toBeGreaterThan(0) liquidPaths.forEach(liquidPath => { const liquidDef = require(liquidPath) @@ -45,22 +42,24 @@ describe('test schema against all liquid specs definitions', () => { expect(valid).toBe(true) }) - it(`parent dir matches pipette model: ${liquidPath}`, () => { - expect(['p10', 'p20', 'p50', 'p300', 'p1000']).toContain( + it(`parent dir matches a liquid class: ${liquidPath}`, () => { + expect(['default', 'lowVolumeDefault']).toContain( path.basename(path.dirname(liquidPath)) ) }) + + it(`second parent dir matches pipette model: ${liquidPath}`, () => { + expect(['p10', 'p20', 'p50', 'p300', 'p1000']).toContain( + path.basename(path.dirname(path.dirname(liquidPath))) + ) + }) }) }) describe('test schema against all geometry specs definitions', () => { const geometryPaths = glob.sync(allGeometryDefinitions) - - beforeAll(() => { - // Make sure definitions path didn't break, which would give you false positives - expect(geometryPaths.length).toBeGreaterThan(0) - }) - + // Make sure definitions path didn't break, which would give you false positives + expect(geometryPaths.length).toBeGreaterThan(0) geometryPaths.forEach(geometryPath => { const geometryDef = require(geometryPath) const geometryParentDir = path.dirname(geometryPath) @@ -88,16 +87,11 @@ describe('test schema against all geometry specs definitions', () => { describe('test schema against all general specs definitions', () => { const generalPaths = glob.sync(allGeneralDefinitions) - - beforeAll(() => { - // Make sure definitions path didn't break, which would give you false positives - expect(generalPaths.length).toBeGreaterThan(0) - }) + expect(generalPaths.length).toBeGreaterThan(0) generalPaths.forEach(generalPath => { - const generalDef = require(generalPath) - it(`${generalPath} validates against schema`, () => { + const generalDef = require(generalPath) const valid = validateGeneralSpecs(generalDef) const validationErrors = validateGeneralSpecs.errors expect(validationErrors).toBe(null) diff --git a/shared-data/js/constants.ts b/shared-data/js/constants.ts index 51de83946cc..c944c7943dc 100644 --- a/shared-data/js/constants.ts +++ b/shared-data/js/constants.ts @@ -1,3 +1,4 @@ +import type { CutoutFixtureId, CutoutId, AddressableAreaName } from '../deck' import type { ModuleType } from './types' // constants for dealing with robot coordinate system (eg in labwareTools) @@ -183,9 +184,153 @@ export const TC_MODULE_LOCATION_OT3: 'A1+B1' = 'A1+B1' export const WEIGHT_OF_96_CHANNEL: '~10kg' = '~10kg' -export const WASTE_CHUTE_SLOT: 'D3' = 'D3' +export const SINGLE_LEFT_CUTOUTS: CutoutId[] = [ + 'cutoutA1', + 'cutoutB1', + 'cutoutC1', + 'cutoutD1', +] + +export const SINGLE_CENTER_CUTOUTS: CutoutId[] = [ + 'cutoutA2', + 'cutoutB2', + 'cutoutC2', + 'cutoutD2', +] + +export const SINGLE_RIGHT_CUTOUTS: CutoutId[] = [ + 'cutoutA3', + 'cutoutB3', + 'cutoutC3', + 'cutoutD3', +] + +export const STAGING_AREA_CUTOUTS: CutoutId[] = [ + 'cutoutA3', + 'cutoutB3', + 'cutoutC3', + 'cutoutD3', +] + +export const WASTE_CHUTE_CUTOUT: 'cutoutD3' = 'cutoutD3' + +export const A1_ADDRESSABLE_AREA: 'A1' = 'A1' +export const A2_ADDRESSABLE_AREA: 'A2' = 'A2' +export const A3_ADDRESSABLE_AREA: 'A3' = 'A3' +export const A4_ADDRESSABLE_AREA: 'A4' = 'A4' +export const B1_ADDRESSABLE_AREA: 'B1' = 'B1' +export const B2_ADDRESSABLE_AREA: 'B2' = 'B2' +export const B3_ADDRESSABLE_AREA: 'B3' = 'B3' +export const B4_ADDRESSABLE_AREA: 'B4' = 'B4' +export const C1_ADDRESSABLE_AREA: 'C1' = 'C1' +export const C2_ADDRESSABLE_AREA: 'C2' = 'C2' +export const C3_ADDRESSABLE_AREA: 'C3' = 'C3' +export const C4_ADDRESSABLE_AREA: 'C4' = 'C4' +export const D1_ADDRESSABLE_AREA: 'D1' = 'D1' +export const D2_ADDRESSABLE_AREA: 'D2' = 'D2' +export const D3_ADDRESSABLE_AREA: 'D3' = 'D3' +export const D4_ADDRESSABLE_AREA: 'D4' = 'D4' + +export const MOVABLE_TRASH_A1_ADDRESSABLE_AREA: 'movableTrashA1' = + 'movableTrashA1' +export const MOVABLE_TRASH_A3_ADDRESSABLE_AREA: 'movableTrashA3' = + 'movableTrashA3' +export const MOVABLE_TRASH_B1_ADDRESSABLE_AREA: 'movableTrashB1' = + 'movableTrashB1' +export const MOVABLE_TRASH_B3_ADDRESSABLE_AREA: 'movableTrashB3' = + 'movableTrashB3' +export const MOVABLE_TRASH_C1_ADDRESSABLE_AREA: 'movableTrashC1' = + 'movableTrashC1' +export const MOVABLE_TRASH_C3_ADDRESSABLE_AREA: 'movableTrashC3' = + 'movableTrashC3' +export const MOVABLE_TRASH_D1_ADDRESSABLE_AREA: 'movableTrashD1' = + 'movableTrashD1' +export const MOVABLE_TRASH_D3_ADDRESSABLE_AREA: 'movableTrashD3' = + 'movableTrashD3' + +export const ONE_AND_EIGHT_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA: '1and8ChannelWasteChute' = + '1and8ChannelWasteChute' +export const NINETY_SIX_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA: '96ChannelWasteChute' = + '96ChannelWasteChute' +export const GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA: 'gripperWasteChute' = + 'gripperWasteChute' + +export const FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS: AddressableAreaName[] = [ + A1_ADDRESSABLE_AREA, + A2_ADDRESSABLE_AREA, + A3_ADDRESSABLE_AREA, + B1_ADDRESSABLE_AREA, + B2_ADDRESSABLE_AREA, + B3_ADDRESSABLE_AREA, + C1_ADDRESSABLE_AREA, + C2_ADDRESSABLE_AREA, + C3_ADDRESSABLE_AREA, + D1_ADDRESSABLE_AREA, + D2_ADDRESSABLE_AREA, + D3_ADDRESSABLE_AREA, +] + +export const FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS: AddressableAreaName[] = [ + A4_ADDRESSABLE_AREA, + B4_ADDRESSABLE_AREA, + C4_ADDRESSABLE_AREA, + D4_ADDRESSABLE_AREA, +] + +export const MOVABLE_TRASH_ADDRESSABLE_AREAS: AddressableAreaName[] = [ + MOVABLE_TRASH_A1_ADDRESSABLE_AREA, + MOVABLE_TRASH_A3_ADDRESSABLE_AREA, + MOVABLE_TRASH_B1_ADDRESSABLE_AREA, + MOVABLE_TRASH_B3_ADDRESSABLE_AREA, + MOVABLE_TRASH_C1_ADDRESSABLE_AREA, + MOVABLE_TRASH_C3_ADDRESSABLE_AREA, + MOVABLE_TRASH_D1_ADDRESSABLE_AREA, + MOVABLE_TRASH_D3_ADDRESSABLE_AREA, +] -export const STAGING_AREA_LOAD_NAME = 'stagingArea' -export const STANDARD_SLOT_LOAD_NAME = 'standardSlot' -export const TRASH_BIN_LOAD_NAME = 'trashBin' -export const WASTE_CHUTE_LOAD_NAME = 'wasteChute' +export const WASTE_CHUTE_ADDRESSABLE_AREAS: AddressableAreaName[] = [ + ONE_AND_EIGHT_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA, + NINETY_SIX_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA, + GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA, +] + +export const SINGLE_LEFT_SLOT_FIXTURE: 'singleLeftSlot' = 'singleLeftSlot' +export const SINGLE_CENTER_SLOT_FIXTURE: 'singleCenterSlot' = 'singleCenterSlot' +export const SINGLE_RIGHT_SLOT_FIXTURE: 'singleRightSlot' = 'singleRightSlot' + +export const STAGING_AREA_RIGHT_SLOT_FIXTURE: 'stagingAreaRightSlot' = + 'stagingAreaRightSlot' + +export const TRASH_BIN_ADAPTER_FIXTURE: 'trashBinAdapter' = 'trashBinAdapter' + +export const WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE: 'wasteChuteRightAdapterCovered' = + 'wasteChuteRightAdapterCovered' +export const WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE: 'wasteChuteRightAdapterNoCover' = + 'wasteChuteRightAdapterNoCover' +export const STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE: 'stagingAreaSlotWithWasteChuteRightAdapterCovered' = + 'stagingAreaSlotWithWasteChuteRightAdapterCovered' +export const STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE: 'stagingAreaSlotWithWasteChuteRightAdapterNoCover' = + 'stagingAreaSlotWithWasteChuteRightAdapterNoCover' + +export const SINGLE_SLOT_FIXTURES: CutoutFixtureId[] = [ + SINGLE_LEFT_SLOT_FIXTURE, + SINGLE_CENTER_SLOT_FIXTURE, + SINGLE_RIGHT_SLOT_FIXTURE, +] + +export const WASTE_CHUTE_FIXTURES: CutoutFixtureId[] = [ + WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, +] + +export const WASTE_CHUTE_ONLY_FIXTURES: CutoutFixtureId[] = [ + WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, +] + +export const WASTE_CHUTE_STAGING_AREA_FIXTURES: CutoutFixtureId[] = [ + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, +] diff --git a/shared-data/js/fixtures.ts b/shared-data/js/fixtures.ts index d424464e43d..f5549408af7 100644 --- a/shared-data/js/fixtures.ts +++ b/shared-data/js/fixtures.ts @@ -1,18 +1,161 @@ import { - STAGING_AREA_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, + FLEX_ROBOT_TYPE, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + TRASH_BIN_ADAPTER_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, + WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE, + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, } from './constants' -import type { FixtureLoadName } from './types' - -export function getFixtureDisplayName(loadName: FixtureLoadName): string { - if (loadName === STAGING_AREA_LOAD_NAME) { - return 'Staging Area Slot' - } else if (loadName === TRASH_BIN_LOAD_NAME) { - return 'Trash Bin' - } else if (loadName === WASTE_CHUTE_LOAD_NAME) { - return 'Waste Chute' +import type { CutoutFixtureId } from '../deck' +import type { + AddressableArea, + CoordinateTuple, + Cutout, + DeckDefinition, + OT2Cutout, +} from './types' + +export function getCutoutDisplayName(cutout: Cutout): string { + return cutout.replace('cutout', '') +} + +// mapping of OT-2 deck slots to cutouts +export const OT2_CUTOUT_BY_SLOT_ID: { [slotId: string]: OT2Cutout } = { + 1: 'cutout1', + 2: 'cutout2', + 3: 'cutout3', + 4: 'cutout4', + 5: 'cutout5', + 6: 'cutout6', + 7: 'cutout7', + 8: 'cutout8', + 9: 'cutout9', + 10: 'cutout10', + 11: 'cutout11', +} + +// mapping of Flex deck slots to cutouts +export const FLEX_CUTOUT_BY_SLOT_ID: { [slotId: string]: Cutout } = { + A1: 'cutoutA1', + A2: 'cutoutA2', + A3: 'cutoutA3', + A4: 'cutoutA3', + B1: 'cutoutB1', + B2: 'cutoutB2', + B3: 'cutoutB3', + B4: 'cutoutB3', + C1: 'cutoutC1', + C2: 'cutoutC2', + C3: 'cutoutC3', + C4: 'cutoutC3', + D1: 'cutoutD1', + D2: 'cutoutD2', + D3: 'cutoutD3', + D4: 'cutoutD3', +} + +// returns the position associated with a slot id +export function getPositionFromSlotId( + slotId: string, + deckDef: DeckDefinition +): CoordinateTuple | null { + const cutoutWithSlot = + deckDef.robot.model === FLEX_ROBOT_TYPE + ? FLEX_CUTOUT_BY_SLOT_ID[slotId] + : OT2_CUTOUT_BY_SLOT_ID[slotId] + + const cutoutPosition = + deckDef.locations.cutouts.find(cutout => cutout.id === cutoutWithSlot) + ?.position ?? null + + // adjust for offset from cutout + const offsetFromCutoutFixture = getAddressableAreaFromSlotId(slotId, deckDef) + ?.offsetFromCutoutFixture ?? [0, 0, 0] + + const slotPosition: CoordinateTuple | null = + cutoutPosition != null + ? [ + cutoutPosition[0] + offsetFromCutoutFixture[0], + cutoutPosition[1] + offsetFromCutoutFixture[1], + cutoutPosition[2] + offsetFromCutoutFixture[2], + ] + : null + + return slotPosition +} + +export function getAddressableAreaFromSlotId( + slotId: string, + deckDef: DeckDefinition +): AddressableArea | null { + return ( + deckDef.locations.addressableAreas.find( + addressableArea => addressableArea.id === slotId + ) ?? null + ) +} + +export function getFixtureDisplayName( + cutoutFixtureId: CutoutFixtureId | null +): string { + if (cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE) { + return 'Staging area slot' + } else if (cutoutFixtureId === TRASH_BIN_ADAPTER_FIXTURE) { + return 'Trash bin' + } else if (cutoutFixtureId === WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE) { + return 'Waste chute only' + } else if (cutoutFixtureId === WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE) { + return 'Waste chute only covered' + } else if ( + cutoutFixtureId === + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE + ) { + return 'Waste chute with staging area slot' + } else if ( + cutoutFixtureId === + STAGING_AREA_SLOT_WITH_WASTE_CHUTE_RIGHT_ADAPTER_COVERED_FIXTURE + ) { + return 'Waste chute with staging area slot covered' } else { return 'Slot' } } + +const STANDARD_OT2_SLOTS = [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', +] + +const STANDARD_FLEX_SLOTS = [ + 'A1', + 'A2', + 'A3', + 'B1', + 'B2', + 'B3', + 'C1', + 'C2', + 'C3', + 'D1', + 'D2', + 'D3', +] + +export const isAddressableAreaStandardSlot = ( + addressableAreaId: string, + deckDef: DeckDefinition +): boolean => + (deckDef.robot.model === FLEX_ROBOT_TYPE + ? STANDARD_FLEX_SLOTS + : STANDARD_OT2_SLOTS + ).includes(addressableAreaId) diff --git a/shared-data/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.ts b/shared-data/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.ts index 8a9609b4baf..840753899ce 100644 --- a/shared-data/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.ts +++ b/shared-data/js/helpers/__tests__/getDeckDefFromLoadedLabware.test.ts @@ -1,5 +1,5 @@ -import ot2DeckDef from '../../../deck/definitions/3/ot2_standard.json' -import ot3DeckDef from '../../../deck/definitions/3/ot3_standard.json' +import ot2DeckDef from '../../../deck/definitions/4/ot2_standard.json' +import ot3DeckDef from '../../../deck/definitions/4/ot3_standard.json' import { getDeckDefFromRobotType } from '..' describe('getDeckDefFromRobotType', () => { diff --git a/shared-data/js/helpers/__tests__/getRobotTypeFromLoadedLabware.test.ts b/shared-data/js/helpers/__tests__/getRobotTypeFromLoadedLabware.test.ts deleted file mode 100644 index ecba27c7b89..00000000000 --- a/shared-data/js/helpers/__tests__/getRobotTypeFromLoadedLabware.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getRobotTypeFromLoadedLabware } from '..' -import type { LoadedLabware } from '../..' - -describe('getRobotTypeFromLoadedLabware', () => { - it('should return an OT-2 when an OT-2 trash is loaded into the protocol', () => { - const labware: LoadedLabware[] = [ - { - id: 'fixedTrash', - loadName: 'opentrons_1_trash_1100ml_fixed', - definitionUri: 'opentrons/opentrons_1_trash_1100ml_fixed/1', - location: { - slotName: '12', - }, - }, - ] - expect(getRobotTypeFromLoadedLabware(labware)).toBe('OT-2 Standard') - }) - it('should return an OT-3 when an OT-3 trash is loaded into the protocol', () => { - const labware: LoadedLabware[] = [ - { - id: 'fixedTrash', - loadName: 'opentrons_1_trash_3200ml_fixed', - definitionUri: 'opentrons/opentrons_1_trash_3200ml_fixed/1', - location: { - slotName: '12', - }, - }, - ] - expect(getRobotTypeFromLoadedLabware(labware)).toBe('OT-3 Standard') - }) -}) diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index 7c6cebc3847..970e8c1a596 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -2,12 +2,11 @@ import assert from 'assert' import uniq from 'lodash/uniq' import { OPENTRONS_LABWARE_NAMESPACE } from '../constants' -import standardDeckDefOt2 from '../../deck/definitions/3/ot2_standard.json' -import standardDeckDefOt3 from '../../deck/definitions/3/ot3_standard.json' +import standardOt2DeckDef from '../../deck/definitions/4/ot2_standard.json' +import standardFlexDeckDef from '../../deck/definitions/4/ot3_standard.json' import type { DeckDefinition, LabwareDefinition2, - LoadedLabware, ModuleModel, RobotType, ThermalAdapterName, @@ -202,10 +201,10 @@ export const getWellsDepth = ( export const getSlotHasMatingSurfaceUnitVector = ( deckDef: DeckDefinition, - slotNumber: string + addressableAreaName: string ): boolean => { - const matingSurfaceUnitVector = deckDef.locations.orderedSlots.find( - orderedSlot => orderedSlot.id === slotNumber + const matingSurfaceUnitVector = deckDef.locations.addressableAreas.find( + aa => aa.id === addressableAreaName )?.matingSurfaceUnitVector return Boolean(matingSurfaceUnitVector) @@ -220,14 +219,12 @@ export const getAreSlotsHorizontallyAdjacent = ( } const slotANumber = parseInt(slotNameA) const slotBNumber = parseInt(slotNameB) - if (isNaN(slotBNumber) || isNaN(slotANumber)) { return false } - const orderedSlots = standardDeckDefOt2.locations.orderedSlots + const orderedSlots = standardOt2DeckDef.locations.cutouts // intentionally not substracting by 1 because trash (slot 12) should not count const numSlots = orderedSlots.length - if (slotBNumber > numSlots || slotANumber > numSlots) { return false } @@ -260,7 +257,7 @@ export const getAreSlotsVerticallyAdjacent = ( if (isNaN(slotBNumber) || isNaN(slotANumber)) { return false } - const orderedSlots = standardDeckDefOt2.locations.orderedSlots + const orderedSlots = standardOt2DeckDef.locations.cutouts // intentionally not substracting by 1 because trash (slot 12) should not count const numSlots = orderedSlots.length @@ -284,6 +281,9 @@ export const getAreSlotsVerticallyAdjacent = ( return areSlotsVerticallyAdjacent } +// TODO(jr, 11/12/23): rename this utility to mention that it +// is only used in the OT-2, same with getAreSlotsHorizontallyAdjacent +// and getAreSlotsVerticallyAdjacent export const getAreSlotsAdjacent = ( slotNameA?: string | null, slotNameB?: string | null @@ -336,18 +336,11 @@ export const getCalibrationAdapterLoadName = ( } } -export const getRobotTypeFromLoadedLabware = ( - labware: LoadedLabware[] -): RobotType => { - const isProtocolForOT3 = labware.some( - l => l.loadName === 'opentrons_1_trash_3200ml_fixed' - ) - return isProtocolForOT3 ? 'OT-3 Standard' : 'OT-2 Standard' -} - export const getDeckDefFromRobotType = ( robotType: RobotType ): DeckDefinition => { // @ts-expect-error imported JSON not playing nice with TS. see https://github.com/microsoft/TypeScript/issues/32063 - return robotType === 'OT-3 Standard' ? standardDeckDefOt3 : standardDeckDefOt2 + return robotType === 'OT-3 Standard' + ? standardFlexDeckDef + : standardOt2DeckDef } diff --git a/shared-data/js/index.ts b/shared-data/js/index.ts index 61fe3babedf..ce7335fa426 100644 --- a/shared-data/js/index.ts +++ b/shared-data/js/index.ts @@ -8,6 +8,7 @@ export * from './modules' export * from './fixtures' export * from './gripper' export * from '../protocol' +export * from '../deck' export * from './titleCase' export * from './errors' export * from './fixtures' diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 07b5302b63d..3570d42ddbf 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -24,15 +24,11 @@ import { GRIPPER_V1_2, EXTENSION, MAGNETIC_BLOCK_V1, - STAGING_AREA_LOAD_NAME, - STANDARD_SLOT_LOAD_NAME, - TRASH_BIN_LOAD_NAME, - WASTE_CHUTE_LOAD_NAME, } from './constants' import type { INode } from 'svgson' -import type { RunTimeCommand } from '../command/types' +import type { RunTimeCommand, LabwareLocation } from '../command/types' +import type { AddressableAreaName, CutoutFixtureId, CutoutId } from '../deck' import type { PipetteName } from './pipettes' -import type { LabwareLocation } from '../protocol/types/schemaV7/command/setup' export type RobotType = 'OT-2 Standard' | 'OT-3 Standard' @@ -232,18 +228,6 @@ export type ModuleModelWithLegacy = | typeof MAGDECK | typeof TEMPDECK -export type FixtureLoadName = - | typeof STAGING_AREA_LOAD_NAME - | typeof STANDARD_SLOT_LOAD_NAME - | typeof TRASH_BIN_LOAD_NAME - | typeof WASTE_CHUTE_LOAD_NAME - -export interface DeckOffset { - x: number - y: number - z: number -} - export interface Dimensions { xDimension: number yDimension: number @@ -254,13 +238,6 @@ export interface DeckRobot { model: RobotType } -export interface DeckFixture { - id: string - slot: string - labware: string - displayName: string -} - export type CoordinateTuple = [number, number, number] export type UnitDirection = 1 | -1 @@ -284,10 +261,27 @@ export interface DeckCalibrationPoint { displayName: string } -export interface DeckLocations { - orderedSlots: DeckSlot[] - calibrationPoints: DeckCalibrationPoint[] - fixtures: DeckFixture[] +export interface CutoutFixture { + id: CutoutFixtureId + mayMountTo: CutoutId[] + displayName: string + providesAddressableAreas: Record +} + +type AreaType = 'slot' | 'movableTrash' | 'wasteChute' | 'fixedTrash' + +export interface AddressableArea { + id: AddressableAreaName + areaType: AreaType + offsetFromCutoutFixture: CoordinateTuple + boundingBox: Dimensions + displayName: string + compatibleModuleTypes: ModuleType[] + ableToDropLabware?: boolean + ableToDropTips?: boolean + dropLabwareOffset?: CoordinateTuple + dropTipsOffset?: CoordinateTuple + matingSurfaceUnitVector?: UnitVectorTuple } export interface DeckMetadata { @@ -295,6 +289,26 @@ export interface DeckMetadata { tags: string[] } +export interface DeckCutout { + id: string + position: CoordinateTuple + displayName: string +} + +export interface LegacyFixture { + id: string + slot: string + labware: string + displayName: string +} + +export interface DeckLocations { + addressableAreas: AddressableArea[] + calibrationPoints: DeckCalibrationPoint[] + cutouts: DeckCutout[] + legacyFixtures: LegacyFixture[] +} + export interface DeckDefinition { otId: string cornerOffsetFromOrigin: CoordinateTuple @@ -302,7 +316,7 @@ export interface DeckDefinition { robot: DeckRobot locations: DeckLocations metadata: DeckMetadata - layers: INode[] + cutoutFixtures: CutoutFixture[] } export interface ModuleDimensions { @@ -459,6 +473,7 @@ export interface CompletedProtocolAnalysis { liquids: Liquid[] commands: RunTimeCommand[] errors: AnalysisError[] + robotType?: RobotType | null } export interface ResourceFile { @@ -531,8 +546,35 @@ export type StatusBarAnimation = export type StatusBarAnimations = StatusBarAnimation[] -// TODO(bh, 2023-09-28): refine types when settled export type Cutout = + | 'cutoutA1' + | 'cutoutB1' + | 'cutoutC1' + | 'cutoutD1' + | 'cutoutA2' + | 'cutoutB2' + | 'cutoutC2' + | 'cutoutD2' + | 'cutoutA3' + | 'cutoutB3' + | 'cutoutC3' + | 'cutoutD3' + +export type OT2Cutout = + | 'cutout1' + | 'cutout2' + | 'cutout3' + | 'cutout4' + | 'cutout5' + | 'cutout6' + | 'cutout7' + | 'cutout8' + | 'cutout9' + | 'cutout10' + | 'cutout11' + | 'cutout12' + +export type FlexSlot = | 'A1' | 'B1' | 'C1' @@ -545,11 +587,14 @@ export type Cutout = | 'B3' | 'C3' | 'D3' + | 'A4' + | 'B4' + | 'C4' + | 'D4' -export interface Fixture { - fixtureId: string - fixtureLocation: Cutout - loadName: FixtureLoadName +export interface CutoutConfig { + cutoutId: CutoutId + cutoutFixtureId: CutoutFixtureId | null } -export type DeckConfiguration = Fixture[] +export type DeckConfiguration = CutoutConfig[] diff --git a/shared-data/labware/definitions/2/armadillo_96_wellplate_200ul_pcr_full_skirt/2.json b/shared-data/labware/definitions/2/armadillo_96_wellplate_200ul_pcr_full_skirt/2.json index f0704d43cca..dec71b2b4e1 100644 --- a/shared-data/labware/definitions/2/armadillo_96_wellplate_200ul_pcr_full_skirt/2.json +++ b/shared-data/labware/definitions/2/armadillo_96_wellplate_200ul_pcr_full_skirt/2.json @@ -55,7 +55,7 @@ "z": 10.7 } }, - "gripForce": 15, + "gripForce": 9, "gripHeightFromLabwareBottom": 10, "ordering": [ ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], diff --git a/shared-data/labware/definitions/2/opentrons_96_pcr_adapter/1.json b/shared-data/labware/definitions/2/opentrons_96_pcr_adapter/1.json index 9447d404d0c..459922d3f22 100644 --- a/shared-data/labware/definitions/2/opentrons_96_pcr_adapter/1.json +++ b/shared-data/labware/definitions/2/opentrons_96_pcr_adapter/1.json @@ -1014,5 +1014,19 @@ "x": 8.5, "y": 5.5, "z": 0 + }, + "gripperOffsets": { + "default": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 0, + "y": 0, + "z": 1.0 + } + } } } diff --git a/shared-data/labware/definitions/2/opentrons_96_well_aluminum_block/1.json b/shared-data/labware/definitions/2/opentrons_96_well_aluminum_block/1.json index 01db61ae047..2eadb732440 100644 --- a/shared-data/labware/definitions/2/opentrons_96_well_aluminum_block/1.json +++ b/shared-data/labware/definitions/2/opentrons_96_well_aluminum_block/1.json @@ -1014,5 +1014,19 @@ "x": 0, "y": 0, "z": 0 + }, + "gripperOffsets": { + "default": { + "pickUpOffset": { + "x": 0, + "y": 0, + "z": 0 + }, + "dropOffset": { + "x": 0, + "y": 0, + "z": 1.0 + } + } } } diff --git a/shared-data/labware/definitions/2/opentrons_96_wellplate_200ul_pcr_full_skirt/2.json b/shared-data/labware/definitions/2/opentrons_96_wellplate_200ul_pcr_full_skirt/2.json index fb6420bac45..4e8314698aa 100644 --- a/shared-data/labware/definitions/2/opentrons_96_wellplate_200ul_pcr_full_skirt/2.json +++ b/shared-data/labware/definitions/2/opentrons_96_wellplate_200ul_pcr_full_skirt/2.json @@ -55,7 +55,7 @@ "z": 10.7 } }, - "gripForce": 15, + "gripForce": 9, "gripHeightFromLabwareBottom": 10, "ordering": [ ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], diff --git a/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_1000ul.json b/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_1000ul.json new file mode 100644 index 00000000000..5eaa01f9440 --- /dev/null +++ b/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_1000ul.json @@ -0,0 +1,1026 @@ +{ + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "brand": { + "brand": "Fixture", + "brandId": [] + }, + "metadata": { + "displayName": "Fixture Flex Tiprack 1000 uL", + "displayCategory": "tipRack", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99 + }, + "gripForce": 16, + "gripHeightFromLabwareBottom": 23.9, + "wells": { + "A1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 74.38, + "z": 1.5 + }, + "B1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 65.38, + "z": 1.5 + }, + "C1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 56.38, + "z": 1.5 + }, + "D1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 47.38, + "z": 1.5 + }, + "E1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 38.38, + "z": 1.5 + }, + "F1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 29.38, + "z": 1.5 + }, + "G1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 20.38, + "z": 1.5 + }, + "H1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 11.38, + "z": 1.5 + }, + "A2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 74.38, + "z": 1.5 + }, + "B2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 65.38, + "z": 1.5 + }, + "C2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 56.38, + "z": 1.5 + }, + "D2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 47.38, + "z": 1.5 + }, + "E2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 38.38, + "z": 1.5 + }, + "F2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 29.38, + "z": 1.5 + }, + "G2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 20.38, + "z": 1.5 + }, + "H2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 11.38, + "z": 1.5 + }, + "A3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 74.38, + "z": 1.5 + }, + "B3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 65.38, + "z": 1.5 + }, + "C3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 56.38, + "z": 1.5 + }, + "D3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 47.38, + "z": 1.5 + }, + "E3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 38.38, + "z": 1.5 + }, + "F3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 29.38, + "z": 1.5 + }, + "G3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 20.38, + "z": 1.5 + }, + "H3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 11.38, + "z": 1.5 + }, + "A4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 74.38, + "z": 1.5 + }, + "B4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 65.38, + "z": 1.5 + }, + "C4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 56.38, + "z": 1.5 + }, + "D4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 47.38, + "z": 1.5 + }, + "E4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 38.38, + "z": 1.5 + }, + "F4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 29.38, + "z": 1.5 + }, + "G4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 20.38, + "z": 1.5 + }, + "H4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 11.38, + "z": 1.5 + }, + "A5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 74.38, + "z": 1.5 + }, + "B5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 65.38, + "z": 1.5 + }, + "C5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 56.38, + "z": 1.5 + }, + "D5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 47.38, + "z": 1.5 + }, + "E5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 38.38, + "z": 1.5 + }, + "F5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 29.38, + "z": 1.5 + }, + "G5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 20.38, + "z": 1.5 + }, + "H5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 11.38, + "z": 1.5 + }, + "A6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 74.38, + "z": 1.5 + }, + "B6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 65.38, + "z": 1.5 + }, + "C6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 56.38, + "z": 1.5 + }, + "D6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 47.38, + "z": 1.5 + }, + "E6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 38.38, + "z": 1.5 + }, + "F6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 29.38, + "z": 1.5 + }, + "G6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 20.38, + "z": 1.5 + }, + "H6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 11.38, + "z": 1.5 + }, + "A7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 74.38, + "z": 1.5 + }, + "B7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 65.38, + "z": 1.5 + }, + "C7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 56.38, + "z": 1.5 + }, + "D7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 47.38, + "z": 1.5 + }, + "E7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 38.38, + "z": 1.5 + }, + "F7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 29.38, + "z": 1.5 + }, + "G7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 20.38, + "z": 1.5 + }, + "H7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 11.38, + "z": 1.5 + }, + "A8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 74.38, + "z": 1.5 + }, + "B8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 65.38, + "z": 1.5 + }, + "C8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 56.38, + "z": 1.5 + }, + "D8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 47.38, + "z": 1.5 + }, + "E8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 38.38, + "z": 1.5 + }, + "F8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 29.38, + "z": 1.5 + }, + "G8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 20.38, + "z": 1.5 + }, + "H8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 11.38, + "z": 1.5 + }, + "A9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 74.38, + "z": 1.5 + }, + "B9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 65.38, + "z": 1.5 + }, + "C9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 56.38, + "z": 1.5 + }, + "D9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 47.38, + "z": 1.5 + }, + "E9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 38.38, + "z": 1.5 + }, + "F9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 29.38, + "z": 1.5 + }, + "G9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 20.38, + "z": 1.5 + }, + "H9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 11.38, + "z": 1.5 + }, + "A10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 74.38, + "z": 1.5 + }, + "B10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 65.38, + "z": 1.5 + }, + "C10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 56.38, + "z": 1.5 + }, + "D10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 47.38, + "z": 1.5 + }, + "E10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 38.38, + "z": 1.5 + }, + "F10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 29.38, + "z": 1.5 + }, + "G10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 20.38, + "z": 1.5 + }, + "H10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 11.38, + "z": 1.5 + }, + "A11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 74.38, + "z": 1.5 + }, + "B11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 65.38, + "z": 1.5 + }, + "C11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 56.38, + "z": 1.5 + }, + "D11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 47.38, + "z": 1.5 + }, + "E11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 38.38, + "z": 1.5 + }, + "F11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 29.38, + "z": 1.5 + }, + "G11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 20.38, + "z": 1.5 + }, + "H11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 11.38, + "z": 1.5 + }, + "A12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 74.38, + "z": 1.5 + }, + "B12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 65.38, + "z": 1.5 + }, + "C12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 56.38, + "z": 1.5 + }, + "D12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 47.38, + "z": 1.5 + }, + "E12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 38.38, + "z": 1.5 + }, + "F12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 29.38, + "z": 1.5 + }, + "G12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 20.38, + "z": 1.5 + }, + "H12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 11.38, + "z": 1.5 + } + }, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ] + } + ], + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": true, + "tipLength": 95.6, + "tipOverlap": 10.5, + "isMagneticModuleCompatible": false, + "loadName": "fixture_flex_96_tiprack_1000ul" + }, + "namespace": "fixture", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { + "x": 0, + "y": 0, + "z": 0 + }, + "stackingOffsetWithLabware": { + "opentrons_flex_96_tiprack_adapter": { + "x": 0, + "y": 0, + "z": 121 + } + } +} diff --git a/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_adapter.json b/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_adapter.json new file mode 100644 index 00000000000..a97b50810f5 --- /dev/null +++ b/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_adapter.json @@ -0,0 +1,41 @@ +{ + "ordering": [], + "brand": { + "brand": "Fixture", + "brandId": [] + }, + "metadata": { + "displayName": "Fixture Flex 96 Tip Rack Adapter", + "displayCategory": "adapter", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 156.5, + "yDimension": 93, + "zDimension": 132 + }, + "wells": {}, + "groups": [ + { + "metadata": {}, + "wells": [] + } + ], + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "fixture_flex_96_tiprack_adapter" + }, + "namespace": "fixture", + "version": 1, + "schemaVersion": 2, + "allowedRoles": ["adapter"], + "cornerOffsetFromSlot": { + "x": -14.25, + "y": -3.5, + "z": 0 + } +} diff --git a/shared-data/module/definitions/3/heaterShakerModuleV1.json b/shared-data/module/definitions/3/heaterShakerModuleV1.json index 63d6dd31998..30164bd8607 100644 --- a/shared-data/module/definitions/3/heaterShakerModuleV1.json +++ b/shared-data/module/definitions/3/heaterShakerModuleV1.json @@ -43,7 +43,7 @@ "dropOffset": { "x": 0, "y": 0, - "z": 0.5 + "z": 1.0 } } }, diff --git a/shared-data/module/definitions/3/magneticBlockV1.json b/shared-data/module/definitions/3/magneticBlockV1.json index 2dea6a9d2ca..a1fd0a39248 100644 --- a/shared-data/module/definitions/3/magneticBlockV1.json +++ b/shared-data/module/definitions/3/magneticBlockV1.json @@ -38,7 +38,7 @@ "dropOffset": { "x": 0, "y": 0, - "z": 0.5 + "z": 1.0 } } }, diff --git a/shared-data/module/definitions/3/temperatureModuleV2.json b/shared-data/module/definitions/3/temperatureModuleV2.json index cc16e0b1ec4..7e67659eba7 100644 --- a/shared-data/module/definitions/3/temperatureModuleV2.json +++ b/shared-data/module/definitions/3/temperatureModuleV2.json @@ -41,7 +41,7 @@ "dropOffset": { "x": 0, "y": 0, - "z": 0.5 + "z": 1.0 } } }, diff --git a/shared-data/module/definitions/3/thermocyclerModuleV2.json b/shared-data/module/definitions/3/thermocyclerModuleV2.json index 9c7965eb49a..531890def74 100644 --- a/shared-data/module/definitions/3/thermocyclerModuleV2.json +++ b/shared-data/module/definitions/3/thermocyclerModuleV2.json @@ -44,7 +44,7 @@ "dropOffset": { "x": 0, "y": 0, - "z": 4.6 + "z": 5.6 } } }, diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_0.json index 4f353672189..61bb1a9b8e4 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_0.json @@ -4,18 +4,28 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.4, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.1, + "3": 0.15, + "4": 0.2, + "5": 0.25, + "6": 0.3, + "7": 0.35, + "8": 0.4 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_3.json index 9090efaccb2..c8e07ba071b 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_3.json @@ -4,18 +4,28 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.4, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.1, + "3": 0.15, + "4": 0.2, + "5": 0.25, + "6": 0.3, + "7": 0.35, + "8": 0.4 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_4.json index d7967eb7f8e..d673992b6d2 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_4.json @@ -4,18 +4,28 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.4, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.1, + "3": 0.15, + "4": 0.2, + "5": 0.25, + "6": 0.3, + "7": 0.35, + "8": 0.4 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_5.json index 9974fe1c989..dbcb7f8dbc7 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_5.json @@ -4,18 +4,28 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.55, - "speed": 30.0, - "presses": 3, - "increment": 3.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 3.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.14, + "3": 0.21, + "4": 0.28, + "5": 0.34, + "6": 0.41, + "7": 0.48, + "8": 0.55 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_6.json b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_6.json index 9974fe1c989..dbcb7f8dbc7 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p10/1_6.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p10/1_6.json @@ -4,18 +4,28 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.55, - "speed": 30.0, - "presses": 3, - "increment": 3.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 3.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.14, + "3": 0.21, + "4": 0.28, + "5": 0.34, + "6": 0.41, + "7": 0.48, + "8": 0.55 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/1_0.json index f8c2397938d..e557a84a71e 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/1_0.json @@ -1,18 +1,31 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 8-Channel 1000 μL", + "displayName": "Flex 8-Channel 1000 uL", "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.5, - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15, + "2": 0.13, + "3": 0.19, + "4": 0.25, + "5": 0.31, + "6": 0.38, + "7": 0.44, + "8": 0.5 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -51,6 +64,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json index 325e06edfc3..16a0931ec7c 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json @@ -4,15 +4,28 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.5, - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15, + "2": 0.13, + "3": 0.19, + "4": 0.25, + "5": 0.31, + "6": 0.38, + "7": 0.44, + "8": 0.5 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -51,6 +64,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json index 325e06edfc3..16a0931ec7c 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json @@ -4,15 +4,28 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.5, - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15, + "2": 0.13, + "3": 0.19, + "4": 0.25, + "5": 0.31, + "6": 0.38, + "7": 0.44, + "8": 0.5 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -51,6 +64,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json index 74ab62080fe..47ace6a4e52 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json @@ -4,15 +4,28 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.55, - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.14, + "3": 0.21, + "4": 0.28, + "5": 0.34, + "6": 0.41, + "7": 0.48, + "8": 0.55 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -51,6 +64,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json index 74ab62080fe..47ace6a4e52 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json @@ -4,15 +4,28 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.55, - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.14, + "3": 0.21, + "4": 0.28, + "5": 0.34, + "6": 0.41, + "7": 0.48, + "8": 0.55 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -51,6 +64,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p20/2_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p20/2_0.json index 5ff09279c1d..b204977b556 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p20/2_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p20/2_0.json @@ -4,18 +4,28 @@ "model": "p20", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "current": 0.6, - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 11.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 11.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.28, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 15.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 15.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p20/2_1.json b/shared-data/pipette/definitions/2/general/eight_channel/p20/2_1.json index 5ff09279c1d..b204977b556 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p20/2_1.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p20/2_1.json @@ -4,18 +4,28 @@ "model": "p20", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "current": 0.6, - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 11.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 11.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.28, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 15.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 15.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_0.json index fc8202bb490..8a0560e150a 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_0.json @@ -4,18 +4,28 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.6, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.3, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_3.json index 158246e8859..ce299da8595 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_3.json @@ -4,18 +4,28 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.6, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.3, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_4.json index 158246e8859..ce299da8595 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_4.json @@ -4,18 +4,28 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.6, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.3, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_5.json index 707f591d644..c420477d1cb 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/1_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/1_5.json @@ -4,18 +4,28 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.9, - "speed": 30.0, - "presses": 3, - "increment": 3.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 3.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.23, + "3": 0.34, + "4": 0.45, + "5": 0.56, + "6": 0.68, + "7": 0.79, + "8": 0.9 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/2_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/2_0.json index faf79930bbf..3096fd46333 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/2_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/2_0.json @@ -4,18 +4,28 @@ "model": "p300", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "current": 0.8, - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 11.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 11.0, + "currentByTipCount": { + "1": 0.13, + "2": 0.2, + "3": 0.3, + "4": 0.4, + "5": 0.5, + "6": 0.6, + "7": 0.7, + "8": 0.8 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 7.5, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 7.5 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p300/2_1.json b/shared-data/pipette/definitions/2/general/eight_channel/p300/2_1.json index faf79930bbf..3096fd46333 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p300/2_1.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p300/2_1.json @@ -4,18 +4,28 @@ "model": "p300", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "current": 0.8, - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 11.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 11.0, + "currentByTipCount": { + "1": 0.13, + "2": 0.2, + "3": 0.3, + "4": 0.4, + "5": 0.5, + "6": 0.6, + "7": 0.7, + "8": 0.8 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 7.5, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 7.5 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_0.json index 3db295cf144..472de5d3a2e 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_0.json @@ -4,18 +4,28 @@ "model": "p50", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.6, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.3, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_3.json index a930751ec6c..f6542bbe5ae 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_3.json @@ -4,18 +4,28 @@ "model": "p50", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.6, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.3, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_4.json index 187dc21b9b0..dd7d9415c45 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_4.json @@ -4,18 +4,28 @@ "model": "p50", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.6, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.15, + "3": 0.23, + "4": 0.3, + "5": 0.38, + "6": 0.45, + "7": 0.53, + "8": 0.6 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_5.json index c2249446a6b..5e35bb44b29 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/1_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/1_5.json @@ -4,18 +4,28 @@ "model": "p50", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.8, - "speed": 30.0, - "presses": 3, - "increment": 3.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 3.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1, + "2": 0.2, + "3": 0.3, + "4": 0.4, + "5": 0.5, + "6": 0.6, + "7": 0.7, + "8": 0.8 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +55,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json index a6dceda8f18..9a22b968ebc 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json @@ -4,15 +4,28 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.5, - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15, + "2": 0.13, + "3": 0.19, + "4": 0.25, + "5": 0.31, + "6": 0.38, + "7": 0.44, + "8": 0.5 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -57,6 +70,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json index a6dceda8f18..9a22b968ebc 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json @@ -4,15 +4,28 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.5, - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15, + "2": 0.13, + "3": 0.19, + "4": 0.25, + "5": 0.31, + "6": 0.38, + "7": 0.44, + "8": 0.5 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -57,6 +70,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json index c7b005e1049..fef2c2cb87e 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json @@ -4,15 +4,28 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.55, - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.14, + "3": 0.2, + "4": 0.28, + "5": 0.34, + "6": 0.41, + "7": 0.48, + "8": 0.55 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -57,6 +70,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json index c7b005e1049..fef2c2cb87e 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json @@ -4,15 +4,28 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.55, - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.14, + "3": 0.2, + "4": 0.28, + "5": 0.34, + "6": 0.41, + "7": 0.48, + "8": 0.55 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -57,6 +70,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json index be5a36fd7b2..e5ae64d98cc 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json @@ -4,20 +4,45 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 1.5, - "presses": 0.0, - "speed": 5.5, - "increment": 0.0, - "distance": 10.0, - "prep_move_distance": 9.0, - "prep_move_speed": 10.0 + "pressFit": { + "presses": 1, + "increment": 0.0, + "speed": 10.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.25, + "3": 0.3, + "4": 0.35, + "5": 0.4, + "6": 0.45, + "7": 0.5, + "8": 0.55, + "12": 0.19, + "16": 0.25, + "24": 0.38, + "48": 0.75 + } + }, + "camAction": { + "prep_move_distance": 9.0, + "prep_move_speed": 10.0, + "speed": 5.5, + "distance": 10.0, + "connectTiprackDistanceMM": 7.0, + "currentByTipCount": { + "96": 1.5 + } + } }, "dropTipConfigurations": { - "current": 1.5, - "speed": 5.5, - "distance": 26.5, - "prep_move_distance": 16.0, - "prep_move_speed": 10.5 + "camAction": { + "current": 1.5, + "speed": 5.5, + "distance": 26.5, + "prep_move_distance": 16.0, + "prep_move_speed": 10.5 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -57,7 +82,6 @@ "current": 2.0, "speed": 5 }, - "tipPresenceCheckDistanceMM": "8.0", - "connectTiprackDistanceMM": "7.0", - "endTipActionRetractDistanceMM": "2.0" + "tipPresenceCheckDistanceMM": 8.0, + "endTipActionRetractDistanceMM": 2.0 } diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json index ad470621884..3db9976b4e9 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json @@ -4,20 +4,50 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 1.5, - "presses": 0.0, - "speed": 5.5, - "increment": 0.0, - "distance": 10.0, - "prep_move_distance": 9.0, - "prep_move_speed": 10.0 + "pressFit": { + "presses": 1, + "increment": 0.0, + "speed": 10.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.25, + "3": 0.3, + "4": 0.35, + "5": 0.4, + "6": 0.45, + "7": 0.5, + "8": 0.55, + "12": 0.19, + "16": 0.25, + "24": 0.38, + "48": 0.75 + } + }, + "camAction": { + "speed": 5.5, + "distance": 10.0, + "prep_move_distance": 9.0, + "prep_move_speed": 10.0, + "connectTiprackDistanceMM": 7.0, + "currentByTipCount": { + "96": 1.5 + } + } }, "dropTipConfigurations": { - "current": 1.5, - "speed": 5.5, - "distance": 10.5, - "prep_move_distance": 16.0, - "prep_move_speed": 10.0 + "plungerEject": { + "current": 1.5, + "speed": 5.5, + "distance": 10.5 + }, + "camAction": { + "current": 1.5, + "speed": 5.5, + "distance": 10.5, + "prep_move_distance": 16.0, + "prep_move_speed": 10.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,7 +75,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 8, 12, 96] + "availableConfigurations": [1, 8, 12, 16, 24, 48, 96] }, "backCompatNames": [], "channels": 96, @@ -57,7 +87,6 @@ "current": 2.0, "speed": 5 }, - "tipPresenceCheckDistanceMM": "8.0", - "connectTiprackDistanceMM": "7.0", - "endTipActionRetractDistanceMM": "2.0" + "tipPresenceCheckDistanceMM": 8.0, + "endTipActionRetractDistanceMM": 2.0 } diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json index ad470621884..f5108478b2d 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json @@ -4,20 +4,45 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 1.5, - "presses": 0.0, - "speed": 5.5, - "increment": 0.0, - "distance": 10.0, - "prep_move_distance": 9.0, - "prep_move_speed": 10.0 + "pressFit": { + "presses": 1, + "increment": 0.0, + "speed": 10.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.25, + "3": 0.3, + "4": 0.35, + "5": 0.4, + "6": 0.45, + "7": 0.5, + "8": 0.55, + "12": 0.19, + "16": 0.25, + "24": 0.38, + "48": 0.75 + } + }, + "camAction": { + "speed": 5.5, + "distance": 10.0, + "prep_move_distance": 9.0, + "prep_move_speed": 10.0, + "connectTiprackDistanceMM": 7.0, + "currentByTipCount": { + "96": 1.5 + } + } }, "dropTipConfigurations": { - "current": 1.5, - "speed": 5.5, - "distance": 10.5, - "prep_move_distance": 16.0, - "prep_move_speed": 10.0 + "camAction": { + "current": 1.5, + "speed": 5.5, + "distance": 10.5, + "prep_move_distance": 16.0, + "prep_move_speed": 10.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,7 +70,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 8, 12, 96] + "availableConfigurations": [1, 8, 12, 16, 24, 48, 96] }, "backCompatNames": [], "channels": 96, @@ -57,7 +82,6 @@ "current": 2.0, "speed": 5 }, - "tipPresenceCheckDistanceMM": "8.0", - "connectTiprackDistanceMM": "7.0", - "endTipActionRetractDistanceMM": "2.0" + "tipPresenceCheckDistanceMM": 8.0, + "endTipActionRetractDistanceMM": 2.0 } diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json index 8ee7b54a538..c3bb411ab2a 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json @@ -4,20 +4,46 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 1.5, - "presses": 0.0, - "speed": 5.5, - "increment": 0.0, - "distance": 10.0, - "prep_move_distance": 9.0, - "prep_move_speed": 10.0 + "pressFit": { + "presses": 1, + "increment": 0.0, + "speed": 10.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.25, + "3": 0.3, + "4": 0.35, + "5": 0.4, + "6": 0.45, + "7": 0.5, + "8": 0.55, + "12": 0.19, + "16": 0.25, + "24": 0.38, + "48": 0.75 + } + }, + "camAction": { + "speed": 5.5, + "distance": 10.0, + "prep_move_distance": 9.0, + "prep_move_speed": 10.0, + "connectTiprackDistanceMM": 7.0, + + "currentByTipCount": { + "96": 1.5 + } + } }, "dropTipConfigurations": { - "current": 1.5, - "speed": 5.5, - "distance": 10.8, - "prep_move_distance": 19.0, - "prep_move_speed": 10.0 + "camAction": { + "current": 1.5, + "speed": 5.5, + "distance": 10.8, + "prep_move_distance": 19.0, + "prep_move_speed": 10.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,7 +71,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 8, 12, 96] + "availableConfigurations": [1, 8, 12, 16, 24, 48, 96] }, "backCompatNames": [], "channels": 96, @@ -57,7 +83,6 @@ "current": 0.8, "speed": 5 }, - "tipPresenceCheckDistanceMM": "8.0", - "connectTiprackDistanceMM": "7.0", - "endTipActionRetractDistanceMM": "2.0" + "tipPresenceCheckDistanceMM": 8.0, + "endTipActionRetractDistanceMM": 2.0 } diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json index 5fe52525614..264c351d0b5 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json @@ -4,20 +4,45 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 1.5, - "presses": 0.0, - "speed": 5.5, - "increment": 0.0, - "distance": 10.0, - "prep_move_distance": 8.25, - "prep_move_speed": 10.0 + "pressFit": { + "presses": 1, + "increment": 0.0, + "speed": 10.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2, + "2": 0.25, + "3": 0.3, + "4": 0.35, + "5": 0.4, + "6": 0.45, + "7": 0.5, + "8": 0.55, + "12": 0.19, + "16": 0.25, + "24": 0.38, + "48": 0.75 + } + }, + "camAction": { + "speed": 5.5, + "distance": 10.0, + "prep_move_distance": 8.25, + "prep_move_speed": 10.0, + "connectTiprackDistanceMM": 7.0, + "currentByTipCount": { + "96": 1.5 + } + } }, "dropTipConfigurations": { - "current": 1.5, - "speed": 5.5, - "distance": 10.8, - "prep_move_distance": 19.0, - "prep_move_speed": 10.0 + "camAction": { + "current": 1.5, + "speed": 5.5, + "distance": 10.8, + "prep_move_distance": 19.0, + "prep_move_speed": 10.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,7 +70,7 @@ }, "partialTipConfigurations": { "partialTipSupported": true, - "availableConfigurations": [1, 8, 12, 96] + "availableConfigurations": [1, 8, 12, 16, 24, 48, 96] }, "backCompatNames": [], "channels": 96, @@ -57,7 +82,6 @@ "current": 0.8, "speed": 5 }, - "tipPresenceCheckDistanceMM": "8.0", - "connectTiprackDistanceMM": "7.0", - "endTipActionRetractDistanceMM": "2.0" + "tipPresenceCheckDistanceMM": 8.0, + "endTipActionRetractDistanceMM": 2.0 } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p10/1_0.json b/shared-data/pipette/definitions/2/general/single_channel/p10/1_0.json index 87bc9d96d64..802c8c7b89b 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p10/1_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p10/1_0.json @@ -4,18 +4,21 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p10/1_3.json b/shared-data/pipette/definitions/2/general/single_channel/p10/1_3.json index 6ffa340a8b3..d202a50bc01 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p10/1_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p10/1_3.json @@ -4,18 +4,21 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p10/1_4.json b/shared-data/pipette/definitions/2/general/single_channel/p10/1_4.json index abd69fc06d4..e1668578f56 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p10/1_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p10/1_4.json @@ -4,18 +4,21 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p10/1_5.json b/shared-data/pipette/definitions/2/general/single_channel/p10/1_5.json index abd69fc06d4..e1668578f56 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p10/1_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p10/1_5.json @@ -4,18 +4,21 @@ "model": "p10", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_0.json index fdd7421a49d..771b4ca2e80 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_0.json @@ -4,18 +4,21 @@ "model": "p1000", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 15.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 15.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_3.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_3.json index bf9dbf3c137..4aa2f4c192f 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_3.json @@ -4,18 +4,21 @@ "model": "p1000", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 15.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 15.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.7, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.7, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_4.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_4.json index bf9dbf3c137..4aa2f4c192f 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_4.json @@ -4,18 +4,21 @@ "model": "p1000", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 15.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 15.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.7, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.7, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_5.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_5.json index cc2384deda6..d4244fd5d61 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/1_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/1_5.json @@ -4,18 +4,21 @@ "model": "p1000", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.15, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 15.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 15.0, + "currentByTipCount": { + "1": 0.15 + } + } }, "dropTipConfigurations": { - "current": 0.7, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.7, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.5, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_0.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_0.json index 1faf7ce5e5f..f7478e116ec 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_0.json @@ -4,18 +4,21 @@ "model": "p1000", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "current": 0.17, - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 17.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 17.0, + "currentByTipCount": { + "1": 0.17 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 7.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 7.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_1.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_1.json index 0600d164c98..52e697b7dc5 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_1.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_1.json @@ -4,18 +4,21 @@ "model": "p1000", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "current": 0.17, - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 17.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 17.0, + "currentByTipCount": { + "1": 0.17 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 7.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 7.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_2.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_2.json index 0600d164c98..52e697b7dc5 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/2_2.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/2_2.json @@ -4,18 +4,21 @@ "model": "p1000", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "current": 0.17, - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 17.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 17.0, + "currentByTipCount": { + "1": 0.17 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 7.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 7.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json index 1aa35765c0b..320387e75b2 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json @@ -4,15 +4,21 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.15, - "presses": 1, - "speed": 5, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 5, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -39,7 +45,8 @@ } }, "partialTipConfigurations": { - "partialTipSupported": false + "partialTipSupported": false, + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -50,6 +57,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json index 1aa35765c0b..320387e75b2 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json @@ -4,15 +4,21 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.15, - "presses": 1, - "speed": 5, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 5, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -39,7 +45,8 @@ } }, "partialTipConfigurations": { - "partialTipSupported": false + "partialTipSupported": false, + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -50,6 +57,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json index 9f88a64897d..209ff556963 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json @@ -4,15 +4,21 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.2, - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 15 + "plungerEject": { + "current": 1.0, + "speed": 15 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -39,7 +45,8 @@ } }, "partialTipConfigurations": { - "partialTipSupported": false + "partialTipSupported": false, + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -50,6 +57,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json index 9f88a64897d..209ff556963 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json @@ -4,15 +4,21 @@ "model": "p1000", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.2, - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 15 + "plungerEject": { + "current": 1.0, + "speed": 15 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -39,7 +45,8 @@ } }, "partialTipConfigurations": { - "partialTipSupported": false + "partialTipSupported": false, + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -50,6 +57,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p20/2_0.json b/shared-data/pipette/definitions/2/general/single_channel/p20/2_0.json index 26e9a9c8f3f..105d5e8b24b 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p20/2_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p20/2_0.json @@ -4,18 +4,21 @@ "model": "p20", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 14.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 14.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 15.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.0, + "speed": 15.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p20/2_1.json b/shared-data/pipette/definitions/2/general/single_channel/p20/2_1.json index 2a14e95d535..78c0f57cb88 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p20/2_1.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p20/2_1.json @@ -4,18 +4,21 @@ "model": "p20", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 14.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 14.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 15.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.0, + "speed": 15.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p20/2_2.json b/shared-data/pipette/definitions/2/general/single_channel/p20/2_2.json index 2a14e95d535..78c0f57cb88 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p20/2_2.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p20/2_2.json @@ -4,18 +4,21 @@ "model": "p20", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 14.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 14.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 15.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.0, + "speed": 15.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/1_0.json b/shared-data/pipette/definitions/2/general/single_channel/p300/1_0.json index 32b3a0c76cc..889cd38633f 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/1_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/1_0.json @@ -4,18 +4,21 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/1_3.json b/shared-data/pipette/definitions/2/general/single_channel/p300/1_3.json index eb334ea9835..ff662bdb79e 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/1_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/1_3.json @@ -4,18 +4,21 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/1_4.json b/shared-data/pipette/definitions/2/general/single_channel/p300/1_4.json index 72c24e1ebdf..6c36696faa2 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/1_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/1_4.json @@ -4,18 +4,21 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/1_5.json b/shared-data/pipette/definitions/2/general/single_channel/p300/1_5.json index 72c24e1ebdf..6c36696faa2 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/1_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/1_5.json @@ -4,18 +4,21 @@ "model": "p300", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/2_0.json b/shared-data/pipette/definitions/2/general/single_channel/p300/2_0.json index 0d0e56e1368..8166c7dbaeb 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/2_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/2_0.json @@ -4,18 +4,21 @@ "model": "p300", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "current": 0.125, - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 17.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 17.0, + "currentByTipCount": { + "1": 0.125 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 7.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 7.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p300/2_1.json b/shared-data/pipette/definitions/2/general/single_channel/p300/2_1.json index 961c04a067d..4342b7cf7c9 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p300/2_1.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p300/2_1.json @@ -4,18 +4,21 @@ "model": "p300", "displayCategory": "GEN2", "pickUpTipConfigurations": { - "current": 0.125, - "speed": 10.0, - "presses": 1, - "increment": 0.0, - "distance": 17.0 + "pressFit": { + "speed": 10.0, + "presses": 1, + "increment": 0.0, + "distance": 17.0, + "currentByTipCount": { + "1": 0.125 + } + } }, "dropTipConfigurations": { - "current": 1.25, - "speed": 7.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 1.25, + "speed": 7.0 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/1_0.json b/shared-data/pipette/definitions/2/general/single_channel/p50/1_0.json index b413e7fc45f..ef647527024 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/1_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/1_0.json @@ -4,18 +4,21 @@ "model": "p50", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/1_3.json b/shared-data/pipette/definitions/2/general/single_channel/p50/1_3.json index a355b7fa796..4820cd9271e 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/1_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/1_3.json @@ -4,18 +4,21 @@ "model": "p50", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/1_4.json b/shared-data/pipette/definitions/2/general/single_channel/p50/1_4.json index 291b79b5751..1ba3ca439f6 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/1_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/1_4.json @@ -4,18 +4,21 @@ "model": "p50", "displayCategory": "GEN1", "pickUpTipConfigurations": { - "current": 0.1, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -45,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/1_5.json b/shared-data/pipette/definitions/2/general/single_channel/p50/1_5.json index 96f0051da60..1ba3ca439f6 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/1_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/1_5.json @@ -3,21 +3,22 @@ "displayName": "P50 Single-Channel GEN1", "model": "p50", "displayCategory": "GEN1", - "majorVersion": 1, - "minorVersion": 5, "pickUpTipConfigurations": { - "current": 0.1, - "speed": 30.0, - "presses": 3, - "increment": 1.0, - "distance": 10.0 + "pressFit": { + "speed": 30.0, + "presses": 3, + "increment": 1.0, + "distance": 10.0, + "currentByTipCount": { + "1": 0.1 + } + } }, "dropTipConfigurations": { - "current": 0.5, - "speed": 5.0, - "presses": 0, - "increment": 0.0, - "distance": 0.0 + "plungerEject": { + "current": 0.5, + "speed": 5.0 + } }, "plungerMotorConfigurations": { "idle": 0.05, @@ -47,6 +48,5 @@ "plungerHomingConfigurations": { "current": 0.3, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json index 7381b5fe52c..1c81a8b8a2f 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json @@ -4,15 +4,21 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.15, - "presses": 1, - "speed": 5, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 5, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,7 +51,8 @@ } }, "partialTipConfigurations": { - "partialTipSupported": false + "partialTipSupported": false, + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -56,6 +63,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json index 7381b5fe52c..1c81a8b8a2f 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json @@ -4,15 +4,21 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.15, - "presses": 1, - "speed": 5, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 5, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.15 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 10 + "plungerEject": { + "current": 1.0, + "speed": 10 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,7 +51,8 @@ } }, "partialTipConfigurations": { - "partialTipSupported": false + "partialTipSupported": false, + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -56,6 +63,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json index 7c606e35508..43961d8b22a 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json @@ -4,15 +4,21 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.2, - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 15 + "plungerEject": { + "current": 1.0, + "speed": 15 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,7 +51,8 @@ } }, "partialTipConfigurations": { - "partialTipSupported": false + "partialTipSupported": false, + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -56,6 +63,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json index 7c606e35508..43961d8b22a 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json @@ -4,15 +4,21 @@ "model": "p50", "displayCategory": "FLEX", "pickUpTipConfigurations": { - "current": 0.2, - "presses": 1, - "speed": 10, - "increment": 0.0, - "distance": 13.0 + "pressFit": { + "presses": 1, + "speed": 10, + "increment": 0.0, + "distance": 13.0, + "currentByTipCount": { + "1": 0.2 + } + } }, "dropTipConfigurations": { - "current": 1.0, - "speed": 15 + "plungerEject": { + "current": 1.0, + "speed": 15 + } }, "plungerMotorConfigurations": { "idle": 0.3, @@ -45,7 +51,8 @@ } }, "partialTipConfigurations": { - "partialTipSupported": false + "partialTipSupported": false, + "availableConfigurations": null }, "backCompatNames": [], "channels": 1, @@ -56,6 +63,5 @@ "plungerHomingConfigurations": { "current": 1.0, "speed": 30 - }, - "tipPresenceCheckDistanceMM": "0.0" + } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_0.json index 121ab9d8526..d214a493141 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_0.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p10/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], - "B1": [0.0, 40.5, 0.8], - "C1": [0.0, 49.5, 0.8], - "D1": [0.0, 58.5, 0.8], - "E1": [0.0, 67.5, 0.8], - "F1": [0.0, 76.5, 0.8], - "G1": [0.0, 85.5, 0.8], - "H1": [0.0, 94.5, 0.8] + "B1": [0.0, 22.5, 0.8], + "C1": [0.0, 13.5, 0.8], + "D1": [0.0, 4.5, 0.8], + "E1": [0.0, -4.5, 0.8], + "F1": [0.0, -13.5, 0.8], + "G1": [0.0, -22.5, 0.8], + "H1": [0.0, -31.5, 0.8] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_3.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_3.json index 121ab9d8526..d214a493141 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_3.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_3.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p10/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], - "B1": [0.0, 40.5, 0.8], - "C1": [0.0, 49.5, 0.8], - "D1": [0.0, 58.5, 0.8], - "E1": [0.0, 67.5, 0.8], - "F1": [0.0, 76.5, 0.8], - "G1": [0.0, 85.5, 0.8], - "H1": [0.0, 94.5, 0.8] + "B1": [0.0, 22.5, 0.8], + "C1": [0.0, 13.5, 0.8], + "D1": [0.0, 4.5, 0.8], + "E1": [0.0, -4.5, 0.8], + "F1": [0.0, -13.5, 0.8], + "G1": [0.0, -22.5, 0.8], + "H1": [0.0, -31.5, 0.8] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_4.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_4.json index 121ab9d8526..d214a493141 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_4.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_4.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p10/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], - "B1": [0.0, 40.5, 0.8], - "C1": [0.0, 49.5, 0.8], - "D1": [0.0, 58.5, 0.8], - "E1": [0.0, 67.5, 0.8], - "F1": [0.0, 76.5, 0.8], - "G1": [0.0, 85.5, 0.8], - "H1": [0.0, 94.5, 0.8] + "B1": [0.0, 22.5, 0.8], + "C1": [0.0, 13.5, 0.8], + "D1": [0.0, 4.5, 0.8], + "E1": [0.0, -4.5, 0.8], + "F1": [0.0, -13.5, 0.8], + "G1": [0.0, -22.5, 0.8], + "H1": [0.0, -31.5, 0.8] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_5.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_5.json index 121ab9d8526..d214a493141 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_5.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_5.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p10/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], - "B1": [0.0, 40.5, 0.8], - "C1": [0.0, 49.5, 0.8], - "D1": [0.0, 58.5, 0.8], - "E1": [0.0, 67.5, 0.8], - "F1": [0.0, 76.5, 0.8], - "G1": [0.0, 85.5, 0.8], - "H1": [0.0, 94.5, 0.8] + "B1": [0.0, 22.5, 0.8], + "C1": [0.0, 13.5, 0.8], + "D1": [0.0, 4.5, 0.8], + "E1": [0.0, -4.5, 0.8], + "F1": [0.0, -13.5, 0.8], + "G1": [0.0, -22.5, 0.8], + "H1": [0.0, -31.5, 0.8] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_6.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_6.json index 121ab9d8526..d214a493141 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_6.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p10/1_6.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p10/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], - "B1": [0.0, 40.5, 0.8], - "C1": [0.0, 49.5, 0.8], - "D1": [0.0, 58.5, 0.8], - "E1": [0.0, 67.5, 0.8], - "F1": [0.0, 76.5, 0.8], - "G1": [0.0, 85.5, 0.8], - "H1": [0.0, 94.5, 0.8] + "B1": [0.0, 22.5, 0.8], + "C1": [0.0, 13.5, 0.8], + "D1": [0.0, 4.5, 0.8], + "E1": [0.0, -4.5, 0.8], + "F1": [0.0, -13.5, 0.8], + "G1": [0.0, -22.5, 0.8], + "H1": [0.0, -31.5, 0.8] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/1_0.json index 1000fbbc8d5..85dd2ff91e3 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/1_0.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], - "B1": [-8.0, -7.0, -259.15], - "C1": [-8.0, 2.0, -259.15], - "D1": [-8.0, 11.0, -259.15], - "E1": [-8.0, 20.0, -259.15], - "F1": [-8.0, 29.0, -259.15], - "G1": [-8.0, 38.0, -259.15], - "H1": [-8.0, 47.0, -259.15] + "B1": [-8.0, -25.0, -259.15], + "C1": [-8.0, -34.0, -259.15], + "D1": [-8.0, -43.0, -259.15], + "E1": [-8.0, -52.0, -259.15], + "F1": [-8.0, -61.0, -259.15], + "G1": [-8.0, -70.0, -259.15], + "H1": [-8.0, -79.0, -259.15] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_0.json index 1000fbbc8d5..85dd2ff91e3 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_0.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], - "B1": [-8.0, -7.0, -259.15], - "C1": [-8.0, 2.0, -259.15], - "D1": [-8.0, 11.0, -259.15], - "E1": [-8.0, 20.0, -259.15], - "F1": [-8.0, 29.0, -259.15], - "G1": [-8.0, 38.0, -259.15], - "H1": [-8.0, 47.0, -259.15] + "B1": [-8.0, -25.0, -259.15], + "C1": [-8.0, -34.0, -259.15], + "D1": [-8.0, -43.0, -259.15], + "E1": [-8.0, -52.0, -259.15], + "F1": [-8.0, -61.0, -259.15], + "G1": [-8.0, -70.0, -259.15], + "H1": [-8.0, -79.0, -259.15] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_3.json index 1000fbbc8d5..85dd2ff91e3 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_3.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], - "B1": [-8.0, -7.0, -259.15], - "C1": [-8.0, 2.0, -259.15], - "D1": [-8.0, 11.0, -259.15], - "E1": [-8.0, 20.0, -259.15], - "F1": [-8.0, 29.0, -259.15], - "G1": [-8.0, 38.0, -259.15], - "H1": [-8.0, 47.0, -259.15] + "B1": [-8.0, -25.0, -259.15], + "C1": [-8.0, -34.0, -259.15], + "D1": [-8.0, -43.0, -259.15], + "E1": [-8.0, -52.0, -259.15], + "F1": [-8.0, -61.0, -259.15], + "G1": [-8.0, -70.0, -259.15], + "H1": [-8.0, -79.0, -259.15] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_4.json index 1000fbbc8d5..85dd2ff91e3 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_4.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], - "B1": [-8.0, -7.0, -259.15], - "C1": [-8.0, 2.0, -259.15], - "D1": [-8.0, 11.0, -259.15], - "E1": [-8.0, 20.0, -259.15], - "F1": [-8.0, 29.0, -259.15], - "G1": [-8.0, 38.0, -259.15], - "H1": [-8.0, 47.0, -259.15] + "B1": [-8.0, -25.0, -259.15], + "C1": [-8.0, -34.0, -259.15], + "D1": [-8.0, -43.0, -259.15], + "E1": [-8.0, -52.0, -259.15], + "F1": [-8.0, -61.0, -259.15], + "G1": [-8.0, -70.0, -259.15], + "H1": [-8.0, -79.0, -259.15] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_5.json index 1000fbbc8d5..85dd2ff91e3 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p1000/3_5.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], - "B1": [-8.0, -7.0, -259.15], - "C1": [-8.0, 2.0, -259.15], - "D1": [-8.0, 11.0, -259.15], - "E1": [-8.0, 20.0, -259.15], - "F1": [-8.0, 29.0, -259.15], - "G1": [-8.0, 38.0, -259.15], - "H1": [-8.0, 47.0, -259.15] + "B1": [-8.0, -25.0, -259.15], + "C1": [-8.0, -34.0, -259.15], + "D1": [-8.0, -43.0, -259.15], + "E1": [-8.0, -52.0, -259.15], + "F1": [-8.0, -61.0, -259.15], + "G1": [-8.0, -70.0, -259.15], + "H1": [-8.0, -79.0, -259.15] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_0.json index 83a63481226..330b04d4bb8 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_0.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 19.4], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p20/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 19.4], - "B1": [0.0, 40.5, 19.4], - "C1": [0.0, 49.5, 19.4], - "D1": [0.0, 58.5, 19.4], - "E1": [0.0, 67.5, 19.4], - "F1": [0.0, 76.5, 19.4], - "G1": [0.0, 85.5, 19.4], - "H1": [0.0, 94.5, 19.4] + "B1": [0.0, 22.5, 19.4], + "C1": [0.0, 13.5, 19.4], + "D1": [0.0, 4.5, 19.4], + "E1": [0.0, -4.5, 19.4], + "F1": [0.0, -13.5, 19.4], + "G1": [0.0, -22.5, 19.4], + "H1": [0.0, -31.5, 19.4] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_1.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_1.json index 83a63481226..330b04d4bb8 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_1.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p20/2_1.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 19.4], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p20/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 19.4], - "B1": [0.0, 40.5, 19.4], - "C1": [0.0, 49.5, 19.4], - "D1": [0.0, 58.5, 19.4], - "E1": [0.0, 67.5, 19.4], - "F1": [0.0, 76.5, 19.4], - "G1": [0.0, 85.5, 19.4], - "H1": [0.0, 94.5, 19.4] + "B1": [0.0, 22.5, 19.4], + "C1": [0.0, 13.5, 19.4], + "D1": [0.0, 4.5, 19.4], + "E1": [0.0, -4.5, 19.4], + "F1": [0.0, -13.5, 19.4], + "G1": [0.0, -22.5, 19.4], + "H1": [0.0, -31.5, 19.4] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_0.json index 0575b8dca91..0df1381c03e 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_0.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p300/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], - "B1": [0.0, 40.5, 0.8], - "C1": [0.0, 49.5, 0.8], - "D1": [0.0, 58.5, 0.8], - "E1": [0.0, 67.5, 0.8], - "F1": [0.0, 76.5, 0.8], - "G1": [0.0, 85.5, 0.8], - "H1": [0.0, 94.5, 0.8] + "B1": [0.0, 22.5, 0.8], + "C1": [0.0, 13.5, 0.8], + "D1": [0.0, 4.5, 0.8], + "E1": [0.0, -4.5, 0.8], + "F1": [0.0, -13.5, 0.8], + "G1": [0.0, -22.5, 0.8], + "H1": [0.0, -31.5, 0.8] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_3.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_3.json index 0575b8dca91..0df1381c03e 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_3.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_3.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p300/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], - "B1": [0.0, 40.5, 0.8], - "C1": [0.0, 49.5, 0.8], - "D1": [0.0, 58.5, 0.8], - "E1": [0.0, 67.5, 0.8], - "F1": [0.0, 76.5, 0.8], - "G1": [0.0, 85.5, 0.8], - "H1": [0.0, 94.5, 0.8] + "B1": [0.0, 22.5, 0.8], + "C1": [0.0, 13.5, 0.8], + "D1": [0.0, 4.5, 0.8], + "E1": [0.0, -4.5, 0.8], + "F1": [0.0, -13.5, 0.8], + "G1": [0.0, -22.5, 0.8], + "H1": [0.0, -31.5, 0.8] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_4.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_4.json index 0575b8dca91..0df1381c03e 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_4.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_4.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p300/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], - "B1": [0.0, 40.5, 0.8], - "C1": [0.0, 49.5, 0.8], - "D1": [0.0, 58.5, 0.8], - "E1": [0.0, 67.5, 0.8], - "F1": [0.0, 76.5, 0.8], - "G1": [0.0, 85.5, 0.8], - "H1": [0.0, 94.5, 0.8] + "B1": [0.0, 22.5, 0.8], + "C1": [0.0, 13.5, 0.8], + "D1": [0.0, 4.5, 0.8], + "E1": [0.0, -4.5, 0.8], + "F1": [0.0, -13.5, 0.8], + "G1": [0.0, -22.5, 0.8], + "H1": [0.0, -31.5, 0.8] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_5.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_5.json index 0575b8dca91..0df1381c03e 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_5.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/1_5.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p300/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], - "B1": [0.0, 40.5, 0.8], - "C1": [0.0, 49.5, 0.8], - "D1": [0.0, 58.5, 0.8], - "E1": [0.0, 67.5, 0.8], - "F1": [0.0, 76.5, 0.8], - "G1": [0.0, 85.5, 0.8], - "H1": [0.0, 94.5, 0.8] + "B1": [0.0, 22.5, 0.8], + "C1": [0.0, 13.5, 0.8], + "D1": [0.0, 4.5, 0.8], + "E1": [0.0, -4.5, 0.8], + "F1": [0.0, -13.5, 0.8], + "G1": [0.0, -22.5, 0.8], + "H1": [0.0, -31.5, 0.8] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_0.json index d192e50d454..afc2468175b 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_0.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 35.52], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p300/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 35.52], - "B1": [0.0, 40.5, 35.52], - "C1": [0.0, 49.5, 35.52], - "D1": [0.0, 58.5, 35.52], - "E1": [0.0, 67.5, 35.52], - "F1": [0.0, 76.5, 35.52], - "G1": [0.0, 85.5, 35.52], - "H1": [0.0, 94.5, 35.52] + "B1": [0.0, 22.5, 35.52], + "C1": [0.0, 13.5, 35.52], + "D1": [0.0, 4.5, 35.52], + "E1": [0.0, -4.5, 35.52], + "F1": [0.0, -13.5, 35.52], + "G1": [0.0, -22.5, 35.52], + "H1": [0.0, -31.5, 35.52] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_1.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_1.json index d192e50d454..afc2468175b 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_1.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p300/2_1.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 35.52], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p300/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 35.52], - "B1": [0.0, 40.5, 35.52], - "C1": [0.0, 49.5, 35.52], - "D1": [0.0, 58.5, 35.52], - "E1": [0.0, 67.5, 35.52], - "F1": [0.0, 76.5, 35.52], - "G1": [0.0, 85.5, 35.52], - "H1": [0.0, 94.5, 35.52] + "B1": [0.0, 22.5, 35.52], + "C1": [0.0, 13.5, 35.52], + "D1": [0.0, 4.5, 35.52], + "E1": [0.0, -4.5, 35.52], + "F1": [0.0, -13.5, 35.52], + "G1": [0.0, -22.5, 35.52], + "H1": [0.0, -31.5, 35.52] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_0.json index e1e22f72c0c..aa8e5807bb4 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_0.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], - "B1": [0.0, 40.5, 0.8], - "C1": [0.0, 49.5, 0.8], - "D1": [0.0, 58.5, 0.8], - "E1": [0.0, 67.5, 0.8], - "F1": [0.0, 76.5, 0.8], - "G1": [0.0, 85.5, 0.8], - "H1": [0.0, 94.5, 0.8] + "B1": [0.0, 22.5, 0.8], + "C1": [0.0, 13.5, 0.8], + "D1": [0.0, 4.5, 0.8], + "E1": [0.0, -4.5, 0.8], + "F1": [0.0, -13.5, 0.8], + "G1": [0.0, -22.5, 0.8], + "H1": [0.0, -31.5, 0.8] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_3.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_3.json index e1e22f72c0c..aa8e5807bb4 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_3.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_3.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], - "B1": [0.0, 40.5, 0.8], - "C1": [0.0, 49.5, 0.8], - "D1": [0.0, 58.5, 0.8], - "E1": [0.0, 67.5, 0.8], - "F1": [0.0, 76.5, 0.8], - "G1": [0.0, 85.5, 0.8], - "H1": [0.0, 94.5, 0.8] + "B1": [0.0, 22.5, 0.8], + "C1": [0.0, 13.5, 0.8], + "D1": [0.0, 4.5, 0.8], + "E1": [0.0, -4.5, 0.8], + "F1": [0.0, -13.5, 0.8], + "G1": [0.0, -22.5, 0.8], + "H1": [0.0, -31.5, 0.8] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_4.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_4.json index e1e22f72c0c..aa8e5807bb4 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_4.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_4.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], - "B1": [0.0, 40.5, 0.8], - "C1": [0.0, 49.5, 0.8], - "D1": [0.0, 58.5, 0.8], - "E1": [0.0, 67.5, 0.8], - "F1": [0.0, 76.5, 0.8], - "G1": [0.0, 85.5, 0.8], - "H1": [0.0, 94.5, 0.8] + "B1": [0.0, 22.5, 0.8], + "C1": [0.0, 13.5, 0.8], + "D1": [0.0, 4.5, 0.8], + "E1": [0.0, -4.5, 0.8], + "F1": [0.0, -13.5, 0.8], + "G1": [0.0, -22.5, 0.8], + "H1": [0.0, -31.5, 0.8] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_5.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_5.json index e1e22f72c0c..aa8e5807bb4 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_5.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/1_5.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 31.5, 0.8], "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p50/placeholder.gltf", + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [0.0, 31.5, 0.8], - "B1": [0.0, 40.5, 0.8], - "C1": [0.0, 49.5, 0.8], - "D1": [0.0, 58.5, 0.8], - "E1": [0.0, 67.5, 0.8], - "F1": [0.0, 76.5, 0.8], - "G1": [0.0, 85.5, 0.8], - "H1": [0.0, 94.5, 0.8] + "B1": [0.0, 22.5, 0.8], + "C1": [0.0, 13.5, 0.8], + "D1": [0.0, 4.5, 0.8], + "E1": [0.0, -4.5, 0.8], + "F1": [0.0, -13.5, 0.8], + "G1": [0.0, -22.5, 0.8], + "H1": [0.0, -31.5, 0.8] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_0.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_0.json index 0e80b244be2..cea157936fc 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_0.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], - "B1": [-8.0, -7.0, -259.15], - "C1": [-8.0, 2.0, -259.15], - "D1": [-8.0, 11.0, -259.15], - "E1": [-8.0, 20.0, -259.15], - "F1": [-8.0, 29.0, -259.15], - "G1": [-8.0, 38.0, -259.15], - "H1": [-8.0, 47.0, -259.15] + "B1": [-8.0, -25.0, -259.15], + "C1": [-8.0, -34.0, -259.15], + "D1": [-8.0, -43.0, -259.15], + "E1": [-8.0, -52.0, -259.15], + "F1": [-8.0, -61.0, -259.15], + "G1": [-8.0, -70.0, -259.15], + "H1": [-8.0, -79.0, -259.15] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_3.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_3.json index 0e80b244be2..cea157936fc 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_3.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], - "B1": [-8.0, -7.0, -259.15], - "C1": [-8.0, 2.0, -259.15], - "D1": [-8.0, 11.0, -259.15], - "E1": [-8.0, 20.0, -259.15], - "F1": [-8.0, 29.0, -259.15], - "G1": [-8.0, 38.0, -259.15], - "H1": [-8.0, 47.0, -259.15] + "B1": [-8.0, -25.0, -259.15], + "C1": [-8.0, -34.0, -259.15], + "D1": [-8.0, -43.0, -259.15], + "E1": [-8.0, -52.0, -259.15], + "F1": [-8.0, -61.0, -259.15], + "G1": [-8.0, -70.0, -259.15], + "H1": [-8.0, -79.0, -259.15] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_4.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_4.json index 0e80b244be2..cea157936fc 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_4.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], - "B1": [-8.0, -7.0, -259.15], - "C1": [-8.0, 2.0, -259.15], - "D1": [-8.0, 11.0, -259.15], - "E1": [-8.0, 20.0, -259.15], - "F1": [-8.0, 29.0, -259.15], - "G1": [-8.0, 38.0, -259.15], - "H1": [-8.0, 47.0, -259.15] + "B1": [-8.0, -25.0, -259.15], + "C1": [-8.0, -34.0, -259.15], + "D1": [-8.0, -43.0, -259.15], + "E1": [-8.0, -52.0, -259.15], + "F1": [-8.0, -61.0, -259.15], + "G1": [-8.0, -70.0, -259.15], + "H1": [-8.0, -79.0, -259.15] } } diff --git a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_5.json b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_5.json index 0e80b244be2..cea157936fc 100644 --- a/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/geometry/eight_channel/p50/3_5.json @@ -2,14 +2,36 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/eight_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -16.0, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": ["A1"] + }, + { + "key": "B", + "orderedNozzles": ["B1"] + }, + { "key": "C", "orderedNozzles": ["C1"] }, + { "key": "D", "orderedNozzles": ["D1"] }, + { "key": "E", "orderedNozzles": ["E1"] }, + { "key": "F", "orderedNozzles": ["F1"] }, + { "key": "G", "orderedNozzles": ["G1"] }, + { "key": "H", "orderedNozzles": ["H1"] } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + } + ], "nozzleMap": { "A1": [-8.0, -16.0, -259.15], - "B1": [-8.0, -7.0, -259.15], - "C1": [-8.0, 2.0, -259.15], - "D1": [-8.0, 11.0, -259.15], - "E1": [-8.0, 20.0, -259.15], - "F1": [-8.0, 29.0, -259.15], - "G1": [-8.0, 38.0, -259.15], - "H1": [-8.0, 47.0, -259.15] + "B1": [-8.0, -25.0, -259.15], + "C1": [-8.0, -34.0, -259.15], + "D1": [-8.0, -43.0, -259.15], + "E1": [-8.0, -52.0, -259.15], + "F1": [-8.0, -61.0, -259.15], + "G1": [-8.0, -70.0, -259.15], + "H1": [-8.0, -79.0, -259.15] } } diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/1_0.json index e65d6be1e6d..770e7a9af58 100644 --- a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/1_0.json @@ -2,102 +2,290 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/ninety_six_channel/p1000/placeholder.gltf", "nozzleOffset": [-36.0, -25.5, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12" + ] + }, + { + "key": "B", + "orderedNozzles": [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12" + ] + }, + { + "key": "C", + "orderedNozzles": [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12" + ] + }, + { + "key": "D", + "orderedNozzles": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12" + ] + }, + { + "key": "E", + "orderedNozzles": [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12" + ] + }, + { + "key": "F", + "orderedNozzles": [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12" + ] + }, + { + "key": "G", + "orderedNozzles": [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12" + ] + }, + { + "key": "H", + "orderedNozzles": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12" + ] + } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + }, + { + "key": "2", + "orderedNozzles": ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"] + }, + { + "key": "3", + "orderedNozzles": ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"] + }, + { + "key": "4", + "orderedNozzles": ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"] + }, + { + "key": "5", + "orderedNozzles": ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"] + }, + { + "key": "6", + "orderedNozzles": ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"] + }, + { + "key": "7", + "orderedNozzles": ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"] + }, + { + "key": "8", + "orderedNozzles": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"] + }, + { + "key": "9", + "orderedNozzles": ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"] + }, + { + "key": "10", + "orderedNozzles": ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"] + }, + { + "key": "11", + "orderedNozzles": ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"] + }, + { + "key": "12", + "orderedNozzles": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + } + ], "nozzleMap": { "A1": [-36.0, -25.5, -259.15], - "A2": [-45.0, -25.5, -259.15], - "A3": [-54.0, -25.5, -259.15], - "A4": [-63.0, -25.5, -259.15], - "A5": [-72.0, -25.5, -259.15], - "A6": [-81.0, -25.5, -259.15], - "A7": [-90.0, -25.5, -259.15], - "A8": [-99.0, -25.5, -259.15], - "A9": [-108.0, -25.5, -259.15], - "A10": [-117.0, -25.5, -259.15], - "A11": [-126.0, -25.5, -259.15], - "A12": [-135.0, -25.5, -259.15], - "B1": [-36.0, -16.5, -259.15], - "B2": [-45.0, -16.5, -259.15], - "B3": [-54.0, -16.5, -259.15], - "B4": [-63.0, -16.5, -259.15], - "B5": [-72.0, -16.5, -259.15], - "B6": [-81.0, -16.5, -259.15], - "B7": [-90.0, -16.5, -259.15], - "B8": [-99.0, -16.5, -259.15], - "B9": [-108.0, -16.5, -259.15], - "B10": [-117.0, -16.5, -259.15], - "B11": [-126.0, -16.5, -259.15], - "B12": [-135.0, -16.5, -259.15], - "C1": [-36.0, -7.5, -259.15], - "C2": [-45.0, -7.5, -259.15], - "C3": [-54.0, -7.5, -259.15], - "C4": [-63.0, -7.5, -259.15], - "C5": [-72.0, -7.5, -259.15], - "C6": [-81.0, -7.5, -259.15], - "C7": [-90.0, -7.5, -259.15], - "C8": [-99.0, -7.5, -259.15], - "C9": [-108.0, -7.5, -259.15], - "C10": [-117.0, -7.5, -259.15], - "C11": [-126.0, -7.5, -259.15], - "C12": [-135.0, -7.5, -259.15], - "D1": [-36.0, 1.5, -259.15], - "D2": [-45.0, 1.5, -259.15], - "D3": [-54.0, 1.5, -259.15], - "D4": [-63.0, 1.5, -259.15], - "D5": [-72.0, 1.5, -259.15], - "D6": [-81.0, 1.5, -259.15], - "D7": [-90.0, 1.5, -259.15], - "D8": [-99.0, 1.5, -259.15], - "D9": [-108.0, 1.5, -259.15], - "D10": [-117.0, 1.5, -259.15], - "D11": [-126.0, 1.5, -259.15], - "D12": [-135.0, 1.5, -259.15], - "E1": [-36.0, 10.5, -259.15], - "E2": [-45.0, 10.5, -259.15], - "E3": [-54.0, 10.5, -259.15], - "E4": [-63.0, 10.5, -259.15], - "E5": [-72.0, 10.5, -259.15], - "E6": [-81.0, 10.5, -259.15], - "E7": [-90.0, 10.5, -259.15], - "E8": [-99.0, 10.5, -259.15], - "E9": [-108.0, 10.5, -259.15], - "E10": [-117.0, 10.5, -259.15], - "E11": [-126.0, 10.5, -259.15], - "E12": [-135.0, 10.5, -259.15], - "F1": [-36.0, 19.5, -259.15], - "F2": [-45.0, 19.5, -259.15], - "F3": [-54.0, 19.5, -259.15], - "F4": [-63.0, 19.5, -259.15], - "F5": [-72.0, 19.5, -259.15], - "F6": [-81.0, 19.5, -259.15], - "F7": [-90.0, 19.5, -259.15], - "F8": [-99.0, 19.5, -259.15], - "F9": [-108.0, 19.5, -259.15], - "F10": [-117.0, 19.5, -259.15], - "F11": [-126.0, 19.5, -259.15], - "F12": [-135.0, 19.5, -259.15], - "G1": [-36.0, 28.5, -259.15], - "G2": [-45.0, 28.5, -259.15], - "G3": [-54.0, 28.5, -259.15], - "G4": [-63.0, 28.5, -259.15], - "G5": [-72.0, 28.5, -259.15], - "G6": [-81.0, 28.5, -259.15], - "G7": [-90.0, 28.5, -259.15], - "G8": [-99.0, 28.5, -259.15], - "G9": [-108.0, 28.5, -259.15], - "G10": [-117.0, 28.5, -259.15], - "G11": [-126.0, 28.5, -259.15], - "G12": [-135.0, 28.5, -259.15], - "H1": [-36.0, 37.5, -259.15], - "H2": [-45.0, 37.5, -259.15], - "H3": [-54.0, 37.5, -259.15], - "H4": [-63.0, 37.5, -259.15], - "H5": [-72.0, 37.5, -259.15], - "H6": [-81.0, 37.5, -259.15], - "H7": [-90.0, 37.5, -259.15], - "H8": [-99.0, 37.5, -259.15], - "H9": [-108.0, 37.5, -259.15], - "H10": [-117.0, 37.5, -259.15], - "H11": [-126.0, 37.5, -259.15], - "H12": [-135.0, 37.5, -259.15] + "A2": [-27.0, -25.5, -259.15], + "A3": [-18.0, -25.5, -259.15], + "A4": [-9.0, -25.5, -259.15], + "A5": [0.0, -25.5, -259.15], + "A6": [9.0, -25.5, -259.15], + "A7": [18.0, -25.5, -259.15], + "A8": [27.0, -25.5, -259.15], + "A9": [36.0, -25.5, -259.15], + "A10": [45.0, -25.5, -259.15], + "A11": [54.0, -25.5, -259.15], + "A12": [63.0, -25.5, -259.15], + "B1": [-36.0, -34.5, -259.15], + "B2": [-27.0, -34.5, -259.15], + "B3": [-18.0, -34.5, -259.15], + "B4": [-9.0, -34.5, -259.15], + "B5": [0.0, -34.5, -259.15], + "B6": [9.0, -34.5, -259.15], + "B7": [18.0, -34.5, -259.15], + "B8": [27.0, -34.5, -259.15], + "B9": [36.0, -34.5, -259.15], + "B10": [45.0, -34.5, -259.15], + "B11": [54.0, -34.5, -259.15], + "B12": [63.0, -34.5, -259.15], + "C1": [-36.0, -43.5, -259.15], + "C2": [-27.0, -43.5, -259.15], + "C3": [-18.0, -43.5, -259.15], + "C4": [-9.0, -43.5, -259.15], + "C5": [0.0, -43.5, -259.15], + "C6": [9.0, -43.5, -259.15], + "C7": [18.0, -43.5, -259.15], + "C8": [27.0, -43.5, -259.15], + "C9": [36.0, -43.5, -259.15], + "C10": [45.0, -43.5, -259.15], + "C11": [54.0, -43.5, -259.15], + "C12": [63.0, -43.5, -259.15], + "D1": [-36.0, -52.5, -259.15], + "D2": [-27.0, -52.5, -259.15], + "D3": [-18.0, -52.5, -259.15], + "D4": [-9.0, -52.5, -259.15], + "D5": [0.0, -52.5, -259.15], + "D6": [9.0, -52.5, -259.15], + "D7": [18.0, -52.5, -259.15], + "D8": [27.0, -52.5, -259.15], + "D9": [36.0, -52.5, -259.15], + "D10": [45.0, -52.5, -259.15], + "D11": [54.0, -52.5, -259.15], + "D12": [63.0, -52.5, -259.15], + "E1": [-36.0, -61.5, -259.15], + "E2": [-27.0, -61.5, -259.15], + "E3": [-18.0, -61.5, -259.15], + "E4": [-9.0, -61.5, -259.15], + "E5": [0.0, -61.5, -259.15], + "E6": [9.0, -61.5, -259.15], + "E7": [18.0, -61.5, -259.15], + "E8": [27.0, -61.5, -259.15], + "E9": [36.0, -61.5, -259.15], + "E10": [45.0, -61.5, -259.15], + "E11": [54.0, -61.5, -259.15], + "E12": [63.0, -61.5, -259.15], + "F1": [-36.0, -70.5, -259.15], + "F2": [-27.0, -70.5, -259.15], + "F3": [-18.0, -70.5, -259.15], + "F4": [-9.0, -70.5, -259.15], + "F5": [0.0, -70.5, -259.15], + "F6": [9.0, -70.5, -259.15], + "F7": [18.0, -70.5, -259.15], + "F8": [27.0, -70.5, -259.15], + "F9": [36.0, -70.5, -259.15], + "F10": [45.0, -70.5, -259.15], + "F11": [54.0, -70.5, -259.15], + "F12": [63.0, -70.5, -259.15], + "G1": [-36.0, -79.5, -259.15], + "G2": [-27.0, -79.5, -259.15], + "G3": [-18.0, -79.5, -259.15], + "G4": [-9.0, -79.5, -259.15], + "G5": [0.0, -79.5, -259.15], + "G6": [9.0, -79.5, -259.15], + "G7": [18.0, -79.5, -259.15], + "G8": [27.0, -79.5, -259.15], + "G9": [36.0, -79.5, -259.15], + "G10": [45.0, -79.5, -259.15], + "G11": [54.0, -79.5, -259.15], + "G12": [63.0, -79.5, -259.15], + "H1": [-36.0, -88.5, -259.15], + "H2": [-27.0, -88.5, -259.15], + "H3": [-18.0, -88.5, -259.15], + "H4": [-9.0, -88.5, -259.15], + "H5": [0.0, -88.5, -259.15], + "H6": [9.0, -88.5, -259.15], + "H7": [18.0, -88.5, -259.15], + "H8": [27.0, -88.5, -259.15], + "H9": [36.0, -88.5, -259.15], + "H10": [45.0, -88.5, -259.15], + "H11": [54.0, -88.5, -259.15], + "H12": [63.0, -88.5, -259.15] } } diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_0.json index e65d6be1e6d..770e7a9af58 100644 --- a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_0.json @@ -2,102 +2,290 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/ninety_six_channel/p1000/placeholder.gltf", "nozzleOffset": [-36.0, -25.5, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12" + ] + }, + { + "key": "B", + "orderedNozzles": [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12" + ] + }, + { + "key": "C", + "orderedNozzles": [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12" + ] + }, + { + "key": "D", + "orderedNozzles": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12" + ] + }, + { + "key": "E", + "orderedNozzles": [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12" + ] + }, + { + "key": "F", + "orderedNozzles": [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12" + ] + }, + { + "key": "G", + "orderedNozzles": [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12" + ] + }, + { + "key": "H", + "orderedNozzles": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12" + ] + } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + }, + { + "key": "2", + "orderedNozzles": ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"] + }, + { + "key": "3", + "orderedNozzles": ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"] + }, + { + "key": "4", + "orderedNozzles": ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"] + }, + { + "key": "5", + "orderedNozzles": ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"] + }, + { + "key": "6", + "orderedNozzles": ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"] + }, + { + "key": "7", + "orderedNozzles": ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"] + }, + { + "key": "8", + "orderedNozzles": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"] + }, + { + "key": "9", + "orderedNozzles": ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"] + }, + { + "key": "10", + "orderedNozzles": ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"] + }, + { + "key": "11", + "orderedNozzles": ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"] + }, + { + "key": "12", + "orderedNozzles": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + } + ], "nozzleMap": { "A1": [-36.0, -25.5, -259.15], - "A2": [-45.0, -25.5, -259.15], - "A3": [-54.0, -25.5, -259.15], - "A4": [-63.0, -25.5, -259.15], - "A5": [-72.0, -25.5, -259.15], - "A6": [-81.0, -25.5, -259.15], - "A7": [-90.0, -25.5, -259.15], - "A8": [-99.0, -25.5, -259.15], - "A9": [-108.0, -25.5, -259.15], - "A10": [-117.0, -25.5, -259.15], - "A11": [-126.0, -25.5, -259.15], - "A12": [-135.0, -25.5, -259.15], - "B1": [-36.0, -16.5, -259.15], - "B2": [-45.0, -16.5, -259.15], - "B3": [-54.0, -16.5, -259.15], - "B4": [-63.0, -16.5, -259.15], - "B5": [-72.0, -16.5, -259.15], - "B6": [-81.0, -16.5, -259.15], - "B7": [-90.0, -16.5, -259.15], - "B8": [-99.0, -16.5, -259.15], - "B9": [-108.0, -16.5, -259.15], - "B10": [-117.0, -16.5, -259.15], - "B11": [-126.0, -16.5, -259.15], - "B12": [-135.0, -16.5, -259.15], - "C1": [-36.0, -7.5, -259.15], - "C2": [-45.0, -7.5, -259.15], - "C3": [-54.0, -7.5, -259.15], - "C4": [-63.0, -7.5, -259.15], - "C5": [-72.0, -7.5, -259.15], - "C6": [-81.0, -7.5, -259.15], - "C7": [-90.0, -7.5, -259.15], - "C8": [-99.0, -7.5, -259.15], - "C9": [-108.0, -7.5, -259.15], - "C10": [-117.0, -7.5, -259.15], - "C11": [-126.0, -7.5, -259.15], - "C12": [-135.0, -7.5, -259.15], - "D1": [-36.0, 1.5, -259.15], - "D2": [-45.0, 1.5, -259.15], - "D3": [-54.0, 1.5, -259.15], - "D4": [-63.0, 1.5, -259.15], - "D5": [-72.0, 1.5, -259.15], - "D6": [-81.0, 1.5, -259.15], - "D7": [-90.0, 1.5, -259.15], - "D8": [-99.0, 1.5, -259.15], - "D9": [-108.0, 1.5, -259.15], - "D10": [-117.0, 1.5, -259.15], - "D11": [-126.0, 1.5, -259.15], - "D12": [-135.0, 1.5, -259.15], - "E1": [-36.0, 10.5, -259.15], - "E2": [-45.0, 10.5, -259.15], - "E3": [-54.0, 10.5, -259.15], - "E4": [-63.0, 10.5, -259.15], - "E5": [-72.0, 10.5, -259.15], - "E6": [-81.0, 10.5, -259.15], - "E7": [-90.0, 10.5, -259.15], - "E8": [-99.0, 10.5, -259.15], - "E9": [-108.0, 10.5, -259.15], - "E10": [-117.0, 10.5, -259.15], - "E11": [-126.0, 10.5, -259.15], - "E12": [-135.0, 10.5, -259.15], - "F1": [-36.0, 19.5, -259.15], - "F2": [-45.0, 19.5, -259.15], - "F3": [-54.0, 19.5, -259.15], - "F4": [-63.0, 19.5, -259.15], - "F5": [-72.0, 19.5, -259.15], - "F6": [-81.0, 19.5, -259.15], - "F7": [-90.0, 19.5, -259.15], - "F8": [-99.0, 19.5, -259.15], - "F9": [-108.0, 19.5, -259.15], - "F10": [-117.0, 19.5, -259.15], - "F11": [-126.0, 19.5, -259.15], - "F12": [-135.0, 19.5, -259.15], - "G1": [-36.0, 28.5, -259.15], - "G2": [-45.0, 28.5, -259.15], - "G3": [-54.0, 28.5, -259.15], - "G4": [-63.0, 28.5, -259.15], - "G5": [-72.0, 28.5, -259.15], - "G6": [-81.0, 28.5, -259.15], - "G7": [-90.0, 28.5, -259.15], - "G8": [-99.0, 28.5, -259.15], - "G9": [-108.0, 28.5, -259.15], - "G10": [-117.0, 28.5, -259.15], - "G11": [-126.0, 28.5, -259.15], - "G12": [-135.0, 28.5, -259.15], - "H1": [-36.0, 37.5, -259.15], - "H2": [-45.0, 37.5, -259.15], - "H3": [-54.0, 37.5, -259.15], - "H4": [-63.0, 37.5, -259.15], - "H5": [-72.0, 37.5, -259.15], - "H6": [-81.0, 37.5, -259.15], - "H7": [-90.0, 37.5, -259.15], - "H8": [-99.0, 37.5, -259.15], - "H9": [-108.0, 37.5, -259.15], - "H10": [-117.0, 37.5, -259.15], - "H11": [-126.0, 37.5, -259.15], - "H12": [-135.0, 37.5, -259.15] + "A2": [-27.0, -25.5, -259.15], + "A3": [-18.0, -25.5, -259.15], + "A4": [-9.0, -25.5, -259.15], + "A5": [0.0, -25.5, -259.15], + "A6": [9.0, -25.5, -259.15], + "A7": [18.0, -25.5, -259.15], + "A8": [27.0, -25.5, -259.15], + "A9": [36.0, -25.5, -259.15], + "A10": [45.0, -25.5, -259.15], + "A11": [54.0, -25.5, -259.15], + "A12": [63.0, -25.5, -259.15], + "B1": [-36.0, -34.5, -259.15], + "B2": [-27.0, -34.5, -259.15], + "B3": [-18.0, -34.5, -259.15], + "B4": [-9.0, -34.5, -259.15], + "B5": [0.0, -34.5, -259.15], + "B6": [9.0, -34.5, -259.15], + "B7": [18.0, -34.5, -259.15], + "B8": [27.0, -34.5, -259.15], + "B9": [36.0, -34.5, -259.15], + "B10": [45.0, -34.5, -259.15], + "B11": [54.0, -34.5, -259.15], + "B12": [63.0, -34.5, -259.15], + "C1": [-36.0, -43.5, -259.15], + "C2": [-27.0, -43.5, -259.15], + "C3": [-18.0, -43.5, -259.15], + "C4": [-9.0, -43.5, -259.15], + "C5": [0.0, -43.5, -259.15], + "C6": [9.0, -43.5, -259.15], + "C7": [18.0, -43.5, -259.15], + "C8": [27.0, -43.5, -259.15], + "C9": [36.0, -43.5, -259.15], + "C10": [45.0, -43.5, -259.15], + "C11": [54.0, -43.5, -259.15], + "C12": [63.0, -43.5, -259.15], + "D1": [-36.0, -52.5, -259.15], + "D2": [-27.0, -52.5, -259.15], + "D3": [-18.0, -52.5, -259.15], + "D4": [-9.0, -52.5, -259.15], + "D5": [0.0, -52.5, -259.15], + "D6": [9.0, -52.5, -259.15], + "D7": [18.0, -52.5, -259.15], + "D8": [27.0, -52.5, -259.15], + "D9": [36.0, -52.5, -259.15], + "D10": [45.0, -52.5, -259.15], + "D11": [54.0, -52.5, -259.15], + "D12": [63.0, -52.5, -259.15], + "E1": [-36.0, -61.5, -259.15], + "E2": [-27.0, -61.5, -259.15], + "E3": [-18.0, -61.5, -259.15], + "E4": [-9.0, -61.5, -259.15], + "E5": [0.0, -61.5, -259.15], + "E6": [9.0, -61.5, -259.15], + "E7": [18.0, -61.5, -259.15], + "E8": [27.0, -61.5, -259.15], + "E9": [36.0, -61.5, -259.15], + "E10": [45.0, -61.5, -259.15], + "E11": [54.0, -61.5, -259.15], + "E12": [63.0, -61.5, -259.15], + "F1": [-36.0, -70.5, -259.15], + "F2": [-27.0, -70.5, -259.15], + "F3": [-18.0, -70.5, -259.15], + "F4": [-9.0, -70.5, -259.15], + "F5": [0.0, -70.5, -259.15], + "F6": [9.0, -70.5, -259.15], + "F7": [18.0, -70.5, -259.15], + "F8": [27.0, -70.5, -259.15], + "F9": [36.0, -70.5, -259.15], + "F10": [45.0, -70.5, -259.15], + "F11": [54.0, -70.5, -259.15], + "F12": [63.0, -70.5, -259.15], + "G1": [-36.0, -79.5, -259.15], + "G2": [-27.0, -79.5, -259.15], + "G3": [-18.0, -79.5, -259.15], + "G4": [-9.0, -79.5, -259.15], + "G5": [0.0, -79.5, -259.15], + "G6": [9.0, -79.5, -259.15], + "G7": [18.0, -79.5, -259.15], + "G8": [27.0, -79.5, -259.15], + "G9": [36.0, -79.5, -259.15], + "G10": [45.0, -79.5, -259.15], + "G11": [54.0, -79.5, -259.15], + "G12": [63.0, -79.5, -259.15], + "H1": [-36.0, -88.5, -259.15], + "H2": [-27.0, -88.5, -259.15], + "H3": [-18.0, -88.5, -259.15], + "H4": [-9.0, -88.5, -259.15], + "H5": [0.0, -88.5, -259.15], + "H6": [9.0, -88.5, -259.15], + "H7": [18.0, -88.5, -259.15], + "H8": [27.0, -88.5, -259.15], + "H9": [36.0, -88.5, -259.15], + "H10": [45.0, -88.5, -259.15], + "H11": [54.0, -88.5, -259.15], + "H12": [63.0, -88.5, -259.15] } } diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_3.json index e65d6be1e6d..770e7a9af58 100644 --- a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_3.json @@ -2,102 +2,290 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/ninety_six_channel/p1000/placeholder.gltf", "nozzleOffset": [-36.0, -25.5, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12" + ] + }, + { + "key": "B", + "orderedNozzles": [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12" + ] + }, + { + "key": "C", + "orderedNozzles": [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12" + ] + }, + { + "key": "D", + "orderedNozzles": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12" + ] + }, + { + "key": "E", + "orderedNozzles": [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12" + ] + }, + { + "key": "F", + "orderedNozzles": [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12" + ] + }, + { + "key": "G", + "orderedNozzles": [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12" + ] + }, + { + "key": "H", + "orderedNozzles": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12" + ] + } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + }, + { + "key": "2", + "orderedNozzles": ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"] + }, + { + "key": "3", + "orderedNozzles": ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"] + }, + { + "key": "4", + "orderedNozzles": ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"] + }, + { + "key": "5", + "orderedNozzles": ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"] + }, + { + "key": "6", + "orderedNozzles": ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"] + }, + { + "key": "7", + "orderedNozzles": ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"] + }, + { + "key": "8", + "orderedNozzles": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"] + }, + { + "key": "9", + "orderedNozzles": ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"] + }, + { + "key": "10", + "orderedNozzles": ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"] + }, + { + "key": "11", + "orderedNozzles": ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"] + }, + { + "key": "12", + "orderedNozzles": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + } + ], "nozzleMap": { "A1": [-36.0, -25.5, -259.15], - "A2": [-45.0, -25.5, -259.15], - "A3": [-54.0, -25.5, -259.15], - "A4": [-63.0, -25.5, -259.15], - "A5": [-72.0, -25.5, -259.15], - "A6": [-81.0, -25.5, -259.15], - "A7": [-90.0, -25.5, -259.15], - "A8": [-99.0, -25.5, -259.15], - "A9": [-108.0, -25.5, -259.15], - "A10": [-117.0, -25.5, -259.15], - "A11": [-126.0, -25.5, -259.15], - "A12": [-135.0, -25.5, -259.15], - "B1": [-36.0, -16.5, -259.15], - "B2": [-45.0, -16.5, -259.15], - "B3": [-54.0, -16.5, -259.15], - "B4": [-63.0, -16.5, -259.15], - "B5": [-72.0, -16.5, -259.15], - "B6": [-81.0, -16.5, -259.15], - "B7": [-90.0, -16.5, -259.15], - "B8": [-99.0, -16.5, -259.15], - "B9": [-108.0, -16.5, -259.15], - "B10": [-117.0, -16.5, -259.15], - "B11": [-126.0, -16.5, -259.15], - "B12": [-135.0, -16.5, -259.15], - "C1": [-36.0, -7.5, -259.15], - "C2": [-45.0, -7.5, -259.15], - "C3": [-54.0, -7.5, -259.15], - "C4": [-63.0, -7.5, -259.15], - "C5": [-72.0, -7.5, -259.15], - "C6": [-81.0, -7.5, -259.15], - "C7": [-90.0, -7.5, -259.15], - "C8": [-99.0, -7.5, -259.15], - "C9": [-108.0, -7.5, -259.15], - "C10": [-117.0, -7.5, -259.15], - "C11": [-126.0, -7.5, -259.15], - "C12": [-135.0, -7.5, -259.15], - "D1": [-36.0, 1.5, -259.15], - "D2": [-45.0, 1.5, -259.15], - "D3": [-54.0, 1.5, -259.15], - "D4": [-63.0, 1.5, -259.15], - "D5": [-72.0, 1.5, -259.15], - "D6": [-81.0, 1.5, -259.15], - "D7": [-90.0, 1.5, -259.15], - "D8": [-99.0, 1.5, -259.15], - "D9": [-108.0, 1.5, -259.15], - "D10": [-117.0, 1.5, -259.15], - "D11": [-126.0, 1.5, -259.15], - "D12": [-135.0, 1.5, -259.15], - "E1": [-36.0, 10.5, -259.15], - "E2": [-45.0, 10.5, -259.15], - "E3": [-54.0, 10.5, -259.15], - "E4": [-63.0, 10.5, -259.15], - "E5": [-72.0, 10.5, -259.15], - "E6": [-81.0, 10.5, -259.15], - "E7": [-90.0, 10.5, -259.15], - "E8": [-99.0, 10.5, -259.15], - "E9": [-108.0, 10.5, -259.15], - "E10": [-117.0, 10.5, -259.15], - "E11": [-126.0, 10.5, -259.15], - "E12": [-135.0, 10.5, -259.15], - "F1": [-36.0, 19.5, -259.15], - "F2": [-45.0, 19.5, -259.15], - "F3": [-54.0, 19.5, -259.15], - "F4": [-63.0, 19.5, -259.15], - "F5": [-72.0, 19.5, -259.15], - "F6": [-81.0, 19.5, -259.15], - "F7": [-90.0, 19.5, -259.15], - "F8": [-99.0, 19.5, -259.15], - "F9": [-108.0, 19.5, -259.15], - "F10": [-117.0, 19.5, -259.15], - "F11": [-126.0, 19.5, -259.15], - "F12": [-135.0, 19.5, -259.15], - "G1": [-36.0, 28.5, -259.15], - "G2": [-45.0, 28.5, -259.15], - "G3": [-54.0, 28.5, -259.15], - "G4": [-63.0, 28.5, -259.15], - "G5": [-72.0, 28.5, -259.15], - "G6": [-81.0, 28.5, -259.15], - "G7": [-90.0, 28.5, -259.15], - "G8": [-99.0, 28.5, -259.15], - "G9": [-108.0, 28.5, -259.15], - "G10": [-117.0, 28.5, -259.15], - "G11": [-126.0, 28.5, -259.15], - "G12": [-135.0, 28.5, -259.15], - "H1": [-36.0, 37.5, -259.15], - "H2": [-45.0, 37.5, -259.15], - "H3": [-54.0, 37.5, -259.15], - "H4": [-63.0, 37.5, -259.15], - "H5": [-72.0, 37.5, -259.15], - "H6": [-81.0, 37.5, -259.15], - "H7": [-90.0, 37.5, -259.15], - "H8": [-99.0, 37.5, -259.15], - "H9": [-108.0, 37.5, -259.15], - "H10": [-117.0, 37.5, -259.15], - "H11": [-126.0, 37.5, -259.15], - "H12": [-135.0, 37.5, -259.15] + "A2": [-27.0, -25.5, -259.15], + "A3": [-18.0, -25.5, -259.15], + "A4": [-9.0, -25.5, -259.15], + "A5": [0.0, -25.5, -259.15], + "A6": [9.0, -25.5, -259.15], + "A7": [18.0, -25.5, -259.15], + "A8": [27.0, -25.5, -259.15], + "A9": [36.0, -25.5, -259.15], + "A10": [45.0, -25.5, -259.15], + "A11": [54.0, -25.5, -259.15], + "A12": [63.0, -25.5, -259.15], + "B1": [-36.0, -34.5, -259.15], + "B2": [-27.0, -34.5, -259.15], + "B3": [-18.0, -34.5, -259.15], + "B4": [-9.0, -34.5, -259.15], + "B5": [0.0, -34.5, -259.15], + "B6": [9.0, -34.5, -259.15], + "B7": [18.0, -34.5, -259.15], + "B8": [27.0, -34.5, -259.15], + "B9": [36.0, -34.5, -259.15], + "B10": [45.0, -34.5, -259.15], + "B11": [54.0, -34.5, -259.15], + "B12": [63.0, -34.5, -259.15], + "C1": [-36.0, -43.5, -259.15], + "C2": [-27.0, -43.5, -259.15], + "C3": [-18.0, -43.5, -259.15], + "C4": [-9.0, -43.5, -259.15], + "C5": [0.0, -43.5, -259.15], + "C6": [9.0, -43.5, -259.15], + "C7": [18.0, -43.5, -259.15], + "C8": [27.0, -43.5, -259.15], + "C9": [36.0, -43.5, -259.15], + "C10": [45.0, -43.5, -259.15], + "C11": [54.0, -43.5, -259.15], + "C12": [63.0, -43.5, -259.15], + "D1": [-36.0, -52.5, -259.15], + "D2": [-27.0, -52.5, -259.15], + "D3": [-18.0, -52.5, -259.15], + "D4": [-9.0, -52.5, -259.15], + "D5": [0.0, -52.5, -259.15], + "D6": [9.0, -52.5, -259.15], + "D7": [18.0, -52.5, -259.15], + "D8": [27.0, -52.5, -259.15], + "D9": [36.0, -52.5, -259.15], + "D10": [45.0, -52.5, -259.15], + "D11": [54.0, -52.5, -259.15], + "D12": [63.0, -52.5, -259.15], + "E1": [-36.0, -61.5, -259.15], + "E2": [-27.0, -61.5, -259.15], + "E3": [-18.0, -61.5, -259.15], + "E4": [-9.0, -61.5, -259.15], + "E5": [0.0, -61.5, -259.15], + "E6": [9.0, -61.5, -259.15], + "E7": [18.0, -61.5, -259.15], + "E8": [27.0, -61.5, -259.15], + "E9": [36.0, -61.5, -259.15], + "E10": [45.0, -61.5, -259.15], + "E11": [54.0, -61.5, -259.15], + "E12": [63.0, -61.5, -259.15], + "F1": [-36.0, -70.5, -259.15], + "F2": [-27.0, -70.5, -259.15], + "F3": [-18.0, -70.5, -259.15], + "F4": [-9.0, -70.5, -259.15], + "F5": [0.0, -70.5, -259.15], + "F6": [9.0, -70.5, -259.15], + "F7": [18.0, -70.5, -259.15], + "F8": [27.0, -70.5, -259.15], + "F9": [36.0, -70.5, -259.15], + "F10": [45.0, -70.5, -259.15], + "F11": [54.0, -70.5, -259.15], + "F12": [63.0, -70.5, -259.15], + "G1": [-36.0, -79.5, -259.15], + "G2": [-27.0, -79.5, -259.15], + "G3": [-18.0, -79.5, -259.15], + "G4": [-9.0, -79.5, -259.15], + "G5": [0.0, -79.5, -259.15], + "G6": [9.0, -79.5, -259.15], + "G7": [18.0, -79.5, -259.15], + "G8": [27.0, -79.5, -259.15], + "G9": [36.0, -79.5, -259.15], + "G10": [45.0, -79.5, -259.15], + "G11": [54.0, -79.5, -259.15], + "G12": [63.0, -79.5, -259.15], + "H1": [-36.0, -88.5, -259.15], + "H2": [-27.0, -88.5, -259.15], + "H3": [-18.0, -88.5, -259.15], + "H4": [-9.0, -88.5, -259.15], + "H5": [0.0, -88.5, -259.15], + "H6": [9.0, -88.5, -259.15], + "H7": [18.0, -88.5, -259.15], + "H8": [27.0, -88.5, -259.15], + "H9": [36.0, -88.5, -259.15], + "H10": [45.0, -88.5, -259.15], + "H11": [54.0, -88.5, -259.15], + "H12": [63.0, -88.5, -259.15] } } diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_4.json index e65d6be1e6d..770e7a9af58 100644 --- a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_4.json @@ -2,102 +2,290 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/ninety_six_channel/p1000/placeholder.gltf", "nozzleOffset": [-36.0, -25.5, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12" + ] + }, + { + "key": "B", + "orderedNozzles": [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12" + ] + }, + { + "key": "C", + "orderedNozzles": [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12" + ] + }, + { + "key": "D", + "orderedNozzles": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12" + ] + }, + { + "key": "E", + "orderedNozzles": [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12" + ] + }, + { + "key": "F", + "orderedNozzles": [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12" + ] + }, + { + "key": "G", + "orderedNozzles": [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12" + ] + }, + { + "key": "H", + "orderedNozzles": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12" + ] + } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + }, + { + "key": "2", + "orderedNozzles": ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"] + }, + { + "key": "3", + "orderedNozzles": ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"] + }, + { + "key": "4", + "orderedNozzles": ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"] + }, + { + "key": "5", + "orderedNozzles": ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"] + }, + { + "key": "6", + "orderedNozzles": ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"] + }, + { + "key": "7", + "orderedNozzles": ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"] + }, + { + "key": "8", + "orderedNozzles": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"] + }, + { + "key": "9", + "orderedNozzles": ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"] + }, + { + "key": "10", + "orderedNozzles": ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"] + }, + { + "key": "11", + "orderedNozzles": ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"] + }, + { + "key": "12", + "orderedNozzles": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + } + ], "nozzleMap": { "A1": [-36.0, -25.5, -259.15], - "A2": [-45.0, -25.5, -259.15], - "A3": [-54.0, -25.5, -259.15], - "A4": [-63.0, -25.5, -259.15], - "A5": [-72.0, -25.5, -259.15], - "A6": [-81.0, -25.5, -259.15], - "A7": [-90.0, -25.5, -259.15], - "A8": [-99.0, -25.5, -259.15], - "A9": [-108.0, -25.5, -259.15], - "A10": [-117.0, -25.5, -259.15], - "A11": [-126.0, -25.5, -259.15], - "A12": [-135.0, -25.5, -259.15], - "B1": [-36.0, -16.5, -259.15], - "B2": [-45.0, -16.5, -259.15], - "B3": [-54.0, -16.5, -259.15], - "B4": [-63.0, -16.5, -259.15], - "B5": [-72.0, -16.5, -259.15], - "B6": [-81.0, -16.5, -259.15], - "B7": [-90.0, -16.5, -259.15], - "B8": [-99.0, -16.5, -259.15], - "B9": [-108.0, -16.5, -259.15], - "B10": [-117.0, -16.5, -259.15], - "B11": [-126.0, -16.5, -259.15], - "B12": [-135.0, -16.5, -259.15], - "C1": [-36.0, -7.5, -259.15], - "C2": [-45.0, -7.5, -259.15], - "C3": [-54.0, -7.5, -259.15], - "C4": [-63.0, -7.5, -259.15], - "C5": [-72.0, -7.5, -259.15], - "C6": [-81.0, -7.5, -259.15], - "C7": [-90.0, -7.5, -259.15], - "C8": [-99.0, -7.5, -259.15], - "C9": [-108.0, -7.5, -259.15], - "C10": [-117.0, -7.5, -259.15], - "C11": [-126.0, -7.5, -259.15], - "C12": [-135.0, -7.5, -259.15], - "D1": [-36.0, 1.5, -259.15], - "D2": [-45.0, 1.5, -259.15], - "D3": [-54.0, 1.5, -259.15], - "D4": [-63.0, 1.5, -259.15], - "D5": [-72.0, 1.5, -259.15], - "D6": [-81.0, 1.5, -259.15], - "D7": [-90.0, 1.5, -259.15], - "D8": [-99.0, 1.5, -259.15], - "D9": [-108.0, 1.5, -259.15], - "D10": [-117.0, 1.5, -259.15], - "D11": [-126.0, 1.5, -259.15], - "D12": [-135.0, 1.5, -259.15], - "E1": [-36.0, 10.5, -259.15], - "E2": [-45.0, 10.5, -259.15], - "E3": [-54.0, 10.5, -259.15], - "E4": [-63.0, 10.5, -259.15], - "E5": [-72.0, 10.5, -259.15], - "E6": [-81.0, 10.5, -259.15], - "E7": [-90.0, 10.5, -259.15], - "E8": [-99.0, 10.5, -259.15], - "E9": [-108.0, 10.5, -259.15], - "E10": [-117.0, 10.5, -259.15], - "E11": [-126.0, 10.5, -259.15], - "E12": [-135.0, 10.5, -259.15], - "F1": [-36.0, 19.5, -259.15], - "F2": [-45.0, 19.5, -259.15], - "F3": [-54.0, 19.5, -259.15], - "F4": [-63.0, 19.5, -259.15], - "F5": [-72.0, 19.5, -259.15], - "F6": [-81.0, 19.5, -259.15], - "F7": [-90.0, 19.5, -259.15], - "F8": [-99.0, 19.5, -259.15], - "F9": [-108.0, 19.5, -259.15], - "F10": [-117.0, 19.5, -259.15], - "F11": [-126.0, 19.5, -259.15], - "F12": [-135.0, 19.5, -259.15], - "G1": [-36.0, 28.5, -259.15], - "G2": [-45.0, 28.5, -259.15], - "G3": [-54.0, 28.5, -259.15], - "G4": [-63.0, 28.5, -259.15], - "G5": [-72.0, 28.5, -259.15], - "G6": [-81.0, 28.5, -259.15], - "G7": [-90.0, 28.5, -259.15], - "G8": [-99.0, 28.5, -259.15], - "G9": [-108.0, 28.5, -259.15], - "G10": [-117.0, 28.5, -259.15], - "G11": [-126.0, 28.5, -259.15], - "G12": [-135.0, 28.5, -259.15], - "H1": [-36.0, 37.5, -259.15], - "H2": [-45.0, 37.5, -259.15], - "H3": [-54.0, 37.5, -259.15], - "H4": [-63.0, 37.5, -259.15], - "H5": [-72.0, 37.5, -259.15], - "H6": [-81.0, 37.5, -259.15], - "H7": [-90.0, 37.5, -259.15], - "H8": [-99.0, 37.5, -259.15], - "H9": [-108.0, 37.5, -259.15], - "H10": [-117.0, 37.5, -259.15], - "H11": [-126.0, 37.5, -259.15], - "H12": [-135.0, 37.5, -259.15] + "A2": [-27.0, -25.5, -259.15], + "A3": [-18.0, -25.5, -259.15], + "A4": [-9.0, -25.5, -259.15], + "A5": [0.0, -25.5, -259.15], + "A6": [9.0, -25.5, -259.15], + "A7": [18.0, -25.5, -259.15], + "A8": [27.0, -25.5, -259.15], + "A9": [36.0, -25.5, -259.15], + "A10": [45.0, -25.5, -259.15], + "A11": [54.0, -25.5, -259.15], + "A12": [63.0, -25.5, -259.15], + "B1": [-36.0, -34.5, -259.15], + "B2": [-27.0, -34.5, -259.15], + "B3": [-18.0, -34.5, -259.15], + "B4": [-9.0, -34.5, -259.15], + "B5": [0.0, -34.5, -259.15], + "B6": [9.0, -34.5, -259.15], + "B7": [18.0, -34.5, -259.15], + "B8": [27.0, -34.5, -259.15], + "B9": [36.0, -34.5, -259.15], + "B10": [45.0, -34.5, -259.15], + "B11": [54.0, -34.5, -259.15], + "B12": [63.0, -34.5, -259.15], + "C1": [-36.0, -43.5, -259.15], + "C2": [-27.0, -43.5, -259.15], + "C3": [-18.0, -43.5, -259.15], + "C4": [-9.0, -43.5, -259.15], + "C5": [0.0, -43.5, -259.15], + "C6": [9.0, -43.5, -259.15], + "C7": [18.0, -43.5, -259.15], + "C8": [27.0, -43.5, -259.15], + "C9": [36.0, -43.5, -259.15], + "C10": [45.0, -43.5, -259.15], + "C11": [54.0, -43.5, -259.15], + "C12": [63.0, -43.5, -259.15], + "D1": [-36.0, -52.5, -259.15], + "D2": [-27.0, -52.5, -259.15], + "D3": [-18.0, -52.5, -259.15], + "D4": [-9.0, -52.5, -259.15], + "D5": [0.0, -52.5, -259.15], + "D6": [9.0, -52.5, -259.15], + "D7": [18.0, -52.5, -259.15], + "D8": [27.0, -52.5, -259.15], + "D9": [36.0, -52.5, -259.15], + "D10": [45.0, -52.5, -259.15], + "D11": [54.0, -52.5, -259.15], + "D12": [63.0, -52.5, -259.15], + "E1": [-36.0, -61.5, -259.15], + "E2": [-27.0, -61.5, -259.15], + "E3": [-18.0, -61.5, -259.15], + "E4": [-9.0, -61.5, -259.15], + "E5": [0.0, -61.5, -259.15], + "E6": [9.0, -61.5, -259.15], + "E7": [18.0, -61.5, -259.15], + "E8": [27.0, -61.5, -259.15], + "E9": [36.0, -61.5, -259.15], + "E10": [45.0, -61.5, -259.15], + "E11": [54.0, -61.5, -259.15], + "E12": [63.0, -61.5, -259.15], + "F1": [-36.0, -70.5, -259.15], + "F2": [-27.0, -70.5, -259.15], + "F3": [-18.0, -70.5, -259.15], + "F4": [-9.0, -70.5, -259.15], + "F5": [0.0, -70.5, -259.15], + "F6": [9.0, -70.5, -259.15], + "F7": [18.0, -70.5, -259.15], + "F8": [27.0, -70.5, -259.15], + "F9": [36.0, -70.5, -259.15], + "F10": [45.0, -70.5, -259.15], + "F11": [54.0, -70.5, -259.15], + "F12": [63.0, -70.5, -259.15], + "G1": [-36.0, -79.5, -259.15], + "G2": [-27.0, -79.5, -259.15], + "G3": [-18.0, -79.5, -259.15], + "G4": [-9.0, -79.5, -259.15], + "G5": [0.0, -79.5, -259.15], + "G6": [9.0, -79.5, -259.15], + "G7": [18.0, -79.5, -259.15], + "G8": [27.0, -79.5, -259.15], + "G9": [36.0, -79.5, -259.15], + "G10": [45.0, -79.5, -259.15], + "G11": [54.0, -79.5, -259.15], + "G12": [63.0, -79.5, -259.15], + "H1": [-36.0, -88.5, -259.15], + "H2": [-27.0, -88.5, -259.15], + "H3": [-18.0, -88.5, -259.15], + "H4": [-9.0, -88.5, -259.15], + "H5": [0.0, -88.5, -259.15], + "H6": [9.0, -88.5, -259.15], + "H7": [18.0, -88.5, -259.15], + "H8": [27.0, -88.5, -259.15], + "H9": [36.0, -88.5, -259.15], + "H10": [45.0, -88.5, -259.15], + "H11": [54.0, -88.5, -259.15], + "H12": [63.0, -88.5, -259.15] } } diff --git a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_5.json index e65d6be1e6d..770e7a9af58 100644 --- a/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/geometry/ninety_six_channel/p1000/3_5.json @@ -2,102 +2,290 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/ninety_six_channel/p1000/placeholder.gltf", "nozzleOffset": [-36.0, -25.5, -259.15], + "orderedRows": [ + { + "key": "A", + "orderedNozzles": [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12" + ] + }, + { + "key": "B", + "orderedNozzles": [ + "B1", + "B2", + "B3", + "B4", + "B5", + "B6", + "B7", + "B8", + "B9", + "B10", + "B11", + "B12" + ] + }, + { + "key": "C", + "orderedNozzles": [ + "C1", + "C2", + "C3", + "C4", + "C5", + "C6", + "C7", + "C8", + "C9", + "C10", + "C11", + "C12" + ] + }, + { + "key": "D", + "orderedNozzles": [ + "D1", + "D2", + "D3", + "D4", + "D5", + "D6", + "D7", + "D8", + "D9", + "D10", + "D11", + "D12" + ] + }, + { + "key": "E", + "orderedNozzles": [ + "E1", + "E2", + "E3", + "E4", + "E5", + "E6", + "E7", + "E8", + "E9", + "E10", + "E11", + "E12" + ] + }, + { + "key": "F", + "orderedNozzles": [ + "F1", + "F2", + "F3", + "F4", + "F5", + "F6", + "F7", + "F8", + "F9", + "F10", + "F11", + "F12" + ] + }, + { + "key": "G", + "orderedNozzles": [ + "G1", + "G2", + "G3", + "G4", + "G5", + "G6", + "G7", + "G8", + "G9", + "G10", + "G11", + "G12" + ] + }, + { + "key": "H", + "orderedNozzles": [ + "H1", + "H2", + "H3", + "H4", + "H5", + "H6", + "H7", + "H8", + "H9", + "H10", + "H11", + "H12" + ] + } + ], + "orderedColumns": [ + { + "key": "1", + "orderedNozzles": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"] + }, + { + "key": "2", + "orderedNozzles": ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"] + }, + { + "key": "3", + "orderedNozzles": ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"] + }, + { + "key": "4", + "orderedNozzles": ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"] + }, + { + "key": "5", + "orderedNozzles": ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"] + }, + { + "key": "6", + "orderedNozzles": ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"] + }, + { + "key": "7", + "orderedNozzles": ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"] + }, + { + "key": "8", + "orderedNozzles": ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"] + }, + { + "key": "9", + "orderedNozzles": ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"] + }, + { + "key": "10", + "orderedNozzles": ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"] + }, + { + "key": "11", + "orderedNozzles": ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"] + }, + { + "key": "12", + "orderedNozzles": ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + } + ], "nozzleMap": { "A1": [-36.0, -25.5, -259.15], - "A2": [-45.0, -25.5, -259.15], - "A3": [-54.0, -25.5, -259.15], - "A4": [-63.0, -25.5, -259.15], - "A5": [-72.0, -25.5, -259.15], - "A6": [-81.0, -25.5, -259.15], - "A7": [-90.0, -25.5, -259.15], - "A8": [-99.0, -25.5, -259.15], - "A9": [-108.0, -25.5, -259.15], - "A10": [-117.0, -25.5, -259.15], - "A11": [-126.0, -25.5, -259.15], - "A12": [-135.0, -25.5, -259.15], - "B1": [-36.0, -16.5, -259.15], - "B2": [-45.0, -16.5, -259.15], - "B3": [-54.0, -16.5, -259.15], - "B4": [-63.0, -16.5, -259.15], - "B5": [-72.0, -16.5, -259.15], - "B6": [-81.0, -16.5, -259.15], - "B7": [-90.0, -16.5, -259.15], - "B8": [-99.0, -16.5, -259.15], - "B9": [-108.0, -16.5, -259.15], - "B10": [-117.0, -16.5, -259.15], - "B11": [-126.0, -16.5, -259.15], - "B12": [-135.0, -16.5, -259.15], - "C1": [-36.0, -7.5, -259.15], - "C2": [-45.0, -7.5, -259.15], - "C3": [-54.0, -7.5, -259.15], - "C4": [-63.0, -7.5, -259.15], - "C5": [-72.0, -7.5, -259.15], - "C6": [-81.0, -7.5, -259.15], - "C7": [-90.0, -7.5, -259.15], - "C8": [-99.0, -7.5, -259.15], - "C9": [-108.0, -7.5, -259.15], - "C10": [-117.0, -7.5, -259.15], - "C11": [-126.0, -7.5, -259.15], - "C12": [-135.0, -7.5, -259.15], - "D1": [-36.0, 1.5, -259.15], - "D2": [-45.0, 1.5, -259.15], - "D3": [-54.0, 1.5, -259.15], - "D4": [-63.0, 1.5, -259.15], - "D5": [-72.0, 1.5, -259.15], - "D6": [-81.0, 1.5, -259.15], - "D7": [-90.0, 1.5, -259.15], - "D8": [-99.0, 1.5, -259.15], - "D9": [-108.0, 1.5, -259.15], - "D10": [-117.0, 1.5, -259.15], - "D11": [-126.0, 1.5, -259.15], - "D12": [-135.0, 1.5, -259.15], - "E1": [-36.0, 10.5, -259.15], - "E2": [-45.0, 10.5, -259.15], - "E3": [-54.0, 10.5, -259.15], - "E4": [-63.0, 10.5, -259.15], - "E5": [-72.0, 10.5, -259.15], - "E6": [-81.0, 10.5, -259.15], - "E7": [-90.0, 10.5, -259.15], - "E8": [-99.0, 10.5, -259.15], - "E9": [-108.0, 10.5, -259.15], - "E10": [-117.0, 10.5, -259.15], - "E11": [-126.0, 10.5, -259.15], - "E12": [-135.0, 10.5, -259.15], - "F1": [-36.0, 19.5, -259.15], - "F2": [-45.0, 19.5, -259.15], - "F3": [-54.0, 19.5, -259.15], - "F4": [-63.0, 19.5, -259.15], - "F5": [-72.0, 19.5, -259.15], - "F6": [-81.0, 19.5, -259.15], - "F7": [-90.0, 19.5, -259.15], - "F8": [-99.0, 19.5, -259.15], - "F9": [-108.0, 19.5, -259.15], - "F10": [-117.0, 19.5, -259.15], - "F11": [-126.0, 19.5, -259.15], - "F12": [-135.0, 19.5, -259.15], - "G1": [-36.0, 28.5, -259.15], - "G2": [-45.0, 28.5, -259.15], - "G3": [-54.0, 28.5, -259.15], - "G4": [-63.0, 28.5, -259.15], - "G5": [-72.0, 28.5, -259.15], - "G6": [-81.0, 28.5, -259.15], - "G7": [-90.0, 28.5, -259.15], - "G8": [-99.0, 28.5, -259.15], - "G9": [-108.0, 28.5, -259.15], - "G10": [-117.0, 28.5, -259.15], - "G11": [-126.0, 28.5, -259.15], - "G12": [-135.0, 28.5, -259.15], - "H1": [-36.0, 37.5, -259.15], - "H2": [-45.0, 37.5, -259.15], - "H3": [-54.0, 37.5, -259.15], - "H4": [-63.0, 37.5, -259.15], - "H5": [-72.0, 37.5, -259.15], - "H6": [-81.0, 37.5, -259.15], - "H7": [-90.0, 37.5, -259.15], - "H8": [-99.0, 37.5, -259.15], - "H9": [-108.0, 37.5, -259.15], - "H10": [-117.0, 37.5, -259.15], - "H11": [-126.0, 37.5, -259.15], - "H12": [-135.0, 37.5, -259.15] + "A2": [-27.0, -25.5, -259.15], + "A3": [-18.0, -25.5, -259.15], + "A4": [-9.0, -25.5, -259.15], + "A5": [0.0, -25.5, -259.15], + "A6": [9.0, -25.5, -259.15], + "A7": [18.0, -25.5, -259.15], + "A8": [27.0, -25.5, -259.15], + "A9": [36.0, -25.5, -259.15], + "A10": [45.0, -25.5, -259.15], + "A11": [54.0, -25.5, -259.15], + "A12": [63.0, -25.5, -259.15], + "B1": [-36.0, -34.5, -259.15], + "B2": [-27.0, -34.5, -259.15], + "B3": [-18.0, -34.5, -259.15], + "B4": [-9.0, -34.5, -259.15], + "B5": [0.0, -34.5, -259.15], + "B6": [9.0, -34.5, -259.15], + "B7": [18.0, -34.5, -259.15], + "B8": [27.0, -34.5, -259.15], + "B9": [36.0, -34.5, -259.15], + "B10": [45.0, -34.5, -259.15], + "B11": [54.0, -34.5, -259.15], + "B12": [63.0, -34.5, -259.15], + "C1": [-36.0, -43.5, -259.15], + "C2": [-27.0, -43.5, -259.15], + "C3": [-18.0, -43.5, -259.15], + "C4": [-9.0, -43.5, -259.15], + "C5": [0.0, -43.5, -259.15], + "C6": [9.0, -43.5, -259.15], + "C7": [18.0, -43.5, -259.15], + "C8": [27.0, -43.5, -259.15], + "C9": [36.0, -43.5, -259.15], + "C10": [45.0, -43.5, -259.15], + "C11": [54.0, -43.5, -259.15], + "C12": [63.0, -43.5, -259.15], + "D1": [-36.0, -52.5, -259.15], + "D2": [-27.0, -52.5, -259.15], + "D3": [-18.0, -52.5, -259.15], + "D4": [-9.0, -52.5, -259.15], + "D5": [0.0, -52.5, -259.15], + "D6": [9.0, -52.5, -259.15], + "D7": [18.0, -52.5, -259.15], + "D8": [27.0, -52.5, -259.15], + "D9": [36.0, -52.5, -259.15], + "D10": [45.0, -52.5, -259.15], + "D11": [54.0, -52.5, -259.15], + "D12": [63.0, -52.5, -259.15], + "E1": [-36.0, -61.5, -259.15], + "E2": [-27.0, -61.5, -259.15], + "E3": [-18.0, -61.5, -259.15], + "E4": [-9.0, -61.5, -259.15], + "E5": [0.0, -61.5, -259.15], + "E6": [9.0, -61.5, -259.15], + "E7": [18.0, -61.5, -259.15], + "E8": [27.0, -61.5, -259.15], + "E9": [36.0, -61.5, -259.15], + "E10": [45.0, -61.5, -259.15], + "E11": [54.0, -61.5, -259.15], + "E12": [63.0, -61.5, -259.15], + "F1": [-36.0, -70.5, -259.15], + "F2": [-27.0, -70.5, -259.15], + "F3": [-18.0, -70.5, -259.15], + "F4": [-9.0, -70.5, -259.15], + "F5": [0.0, -70.5, -259.15], + "F6": [9.0, -70.5, -259.15], + "F7": [18.0, -70.5, -259.15], + "F8": [27.0, -70.5, -259.15], + "F9": [36.0, -70.5, -259.15], + "F10": [45.0, -70.5, -259.15], + "F11": [54.0, -70.5, -259.15], + "F12": [63.0, -70.5, -259.15], + "G1": [-36.0, -79.5, -259.15], + "G2": [-27.0, -79.5, -259.15], + "G3": [-18.0, -79.5, -259.15], + "G4": [-9.0, -79.5, -259.15], + "G5": [0.0, -79.5, -259.15], + "G6": [9.0, -79.5, -259.15], + "G7": [18.0, -79.5, -259.15], + "G8": [27.0, -79.5, -259.15], + "G9": [36.0, -79.5, -259.15], + "G10": [45.0, -79.5, -259.15], + "G11": [54.0, -79.5, -259.15], + "G12": [63.0, -79.5, -259.15], + "H1": [-36.0, -88.5, -259.15], + "H2": [-27.0, -88.5, -259.15], + "H3": [-18.0, -88.5, -259.15], + "H4": [-9.0, -88.5, -259.15], + "H5": [0.0, -88.5, -259.15], + "H6": [9.0, -88.5, -259.15], + "H7": [18.0, -88.5, -259.15], + "H8": [27.0, -88.5, -259.15], + "H9": [36.0, -88.5, -259.15], + "H10": [45.0, -88.5, -259.15], + "H11": [54.0, -88.5, -259.15], + "H12": [63.0, -88.5, -259.15] } } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_0.json index 8411e4e44dd..46813106020 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 12.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p10/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 12.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_3.json b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_3.json index 8411e4e44dd..46813106020 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_3.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_3.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 12.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p10/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 12.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_4.json b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_4.json index 8411e4e44dd..46813106020 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_4.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_4.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 12.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p10/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 12.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_5.json b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_5.json index 8411e4e44dd..46813106020 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_5.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p10/1_5.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 12.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p10/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 12.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_0.json index 64e0cde09c9..6e0f777ff79 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 45.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 45.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_3.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_3.json index 64e0cde09c9..6e0f777ff79 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_3.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_3.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 45.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 45.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_4.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_4.json index 64e0cde09c9..6e0f777ff79 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_4.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_4.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 45.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 45.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_5.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_5.json index 64e0cde09c9..6e0f777ff79 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_5.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/1_5.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 45.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 45.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_0.json index a69cd078a60..03e44b6b76d 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 50.14], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 50.14] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_1.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_1.json index a69cd078a60..03e44b6b76d 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_1.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_1.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 50.14], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 50.14] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_2.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_2.json index a69cd078a60..03e44b6b76d 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_2.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/2_2.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 50.14], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 50.14] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_0.json index e2a461538c3..50baf715160 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_3.json index e2a461538c3..50baf715160 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_3.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_4.json index e2a461538c3..50baf715160 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_4.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_5.json index e2a461538c3..50baf715160 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p1000/3_5.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_0.json index 30c3eb29e71..db8fa79ea87 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 10.45], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p20/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 10.45] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_1.json b/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_1.json index 30c3eb29e71..db8fa79ea87 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_1.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_1.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 10.45], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p20/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 10.45] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_2.json b/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_2.json index 30c3eb29e71..db8fa79ea87 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_2.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p20/2_2.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 10.45], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p20/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 10.45] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_0.json index 3d97efe13fe..636b42cd596 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p300/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_3.json b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_3.json index 3d97efe13fe..636b42cd596 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_3.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_3.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p300/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_4.json b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_4.json index 3d97efe13fe..636b42cd596 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_4.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_4.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p300/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_5.json b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_5.json index 3d97efe13fe..636b42cd596 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_5.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p300/1_5.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p300/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_0.json index 1b56f8b667a..c43a1fd1f10 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 29.45], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p300/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 29.45] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_1.json b/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_1.json index 1b56f8b667a..c43a1fd1f10 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_1.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p300/2_1.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 29.45], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p300/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 29.45] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_0.json index 1d4a8b299bc..a67e53f92f5 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_3.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_3.json index 1d4a8b299bc..a67e53f92f5 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_3.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_3.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_4.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_4.json index 1d4a8b299bc..a67e53f92f5 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_4.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_4.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_5.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_5.json index 1d4a8b299bc..a67e53f92f5 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_5.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/1_5.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "nozzleOffset": [0.0, 0.0, 25.0], "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [0.0, 0.0, 25.0] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_0.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_0.json index 30252f1ef03..96414e8ec41 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_0.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_3.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_3.json index 30252f1ef03..96414e8ec41 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_3.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_4.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_4.json index 30252f1ef03..96414e8ec41 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_4.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_5.json b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_5.json index 30252f1ef03..96414e8ec41 100644 --- a/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/geometry/single_channel/p50/3_5.json @@ -2,6 +2,8 @@ "$otSharedSchema": "#/pipette/schemas/2/pipetteGeometrySchema.json", "pathTo3D": "pipette/definitions/2/geometry/single_channel/p50/placeholder.gltf", "nozzleOffset": [-8.0, -22.0, -259.15], + "orderedRows": [{ "key": "A", "orderedNozzles": ["A1"] }], + "orderedColumns": [{ "key": "1", "orderedNozzles": ["A1"] }], "nozzleMap": { "A1": [-8.0, -22.0, -259.15] } diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/1_0.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/1_0.json index a3794ba520e..8ca9dc4ece4 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/1_0.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/1_0.json @@ -20,96 +20,36 @@ "aspirate": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, "dispense": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, @@ -134,94 +74,34 @@ "aspirate": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, "dispense": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, @@ -246,94 +126,46 @@ "aspirate": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, "dispense": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_0.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_0.json index a3794ba520e..8ca9dc4ece4 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_0.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_0.json @@ -20,96 +20,36 @@ "aspirate": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, "dispense": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, @@ -134,94 +74,34 @@ "aspirate": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, "dispense": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, @@ -246,94 +126,46 @@ "aspirate": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, "dispense": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json index a3794ba520e..8ca9dc4ece4 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_3.json @@ -20,96 +20,36 @@ "aspirate": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, "dispense": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, @@ -134,94 +74,34 @@ "aspirate": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, "dispense": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, @@ -246,94 +126,46 @@ "aspirate": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, "dispense": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json index a3794ba520e..8ca9dc4ece4 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_4.json @@ -20,96 +20,36 @@ "aspirate": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, "dispense": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, @@ -134,94 +74,34 @@ "aspirate": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, "dispense": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, @@ -246,94 +126,46 @@ "aspirate": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, "dispense": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, diff --git a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json index a3794ba520e..8ca9dc4ece4 100644 --- a/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json +++ b/shared-data/pipette/definitions/2/liquid/ninety_six_channel/p1000/default/3_5.json @@ -20,96 +20,36 @@ "aspirate": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, "dispense": { "default": { "1": [ - [0.4148, -1705.1015, 20.5455], - [0.4476, -80.633, 47.2788], - [0.5512, -1.5936, 11.9026], - [0.6027, -18.9998, 21.4972], - [0.6503, -15.8781, 19.6156], - [0.7733, 3.0612, 7.2993], - [0.8391, -5.2227, 13.7056], - [0.9736, 3.0706, 6.7467], - [1.16, -0.374, 10.1005], - [1.3964, 1.3004, 8.1582], - [1.5815, -0.4837, 10.6494], - [1.8306, 1.1464, 8.0714], - [2.0345, 0.0132, 10.1459], - [2.6221, 0.5374, 9.0794], - [2.9655, -1.7582, 15.0986], - [3.5124, 0.2754, 9.0681], - [4.6591, 1.406, 5.097], - [5.367, 0.394, 9.8123], - [6.0839, 0.3365, 10.1205], - [6.8312, 0.3379, 10.1121], - [7.5676, 0.2611, 10.637], - [8.2397, 0.095, 11.8939], - [8.9776, 0.2015, 11.0165], - [10.413, 0.1332, 11.6294], - [11.8539, 0.1074, 11.8979], - [13.3655, 0.1286, 11.6464], - [14.8236, 0.0758, 12.3519], - [16.3203, 0.083, 12.2457], - [17.7915, 0.0581, 12.6515], - [19.2145, 0.0273, 13.1995], - [20.6718, 0.0388, 12.9792], - [22.1333, 0.0357, 13.044], - [25.0761, 0.0332, 13.0977], - [28.0339, 0.029, 13.2035], - [30.967, 0.0201, 13.4538], - [33.8727, 0.013, 13.6737], - [36.8273, 0.0172, 13.5324], - [39.7594, 0.0121, 13.7191], - [42.6721, 0.0083, 13.8687], - [45.5964, 0.0085, 13.8618], - [48.5297, 0.0084, 13.8668], - [51.4512, 0.0064, 13.9651] + [1.933333, 2.844459, 4.750159], + [2.833333, 1.12901, 8.066694], + [3.603333, 0.254744, 10.543779], + [4.836667, 1.101839, 7.491414], + [5.755, 0.277649, 11.47775], + [6.643333, 0.14813, 12.223126], + [7.548333, 0.145635, 12.239705], + [8.475, 0.15097, 12.199433], + [13.02, 0.071736, 12.870946], + [22.318333, 0.042305, 13.254131], + [36.463333, 0.021195, 13.725284], + [54.82, 0.001805, 14.43229] ] } }, @@ -134,94 +74,34 @@ "aspirate": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, "dispense": { "default": { "1": [ - [0.8314, -2.9322, 24.0741], - [0.8853, -30.0996, 48.7784], - [0.9778, -4.3627, 25.9941], - [0.975, 802.2301, -762.6744], - [1.1272, -4.6837, 24.0666], - [1.2747, -3.91, 23.1945], - [1.5656, -2.8032, 21.7836], - [1.6667, -7.2039, 28.6731], - [2.4403, -0.5147, 17.5244], - [3.0564, -1.6013, 20.1761], - [3.6444, -1.1974, 18.9418], - [4.1189, -1.7877, 21.0928], - [4.6467, -0.8591, 17.2684], - [5.2597, -0.207, 14.2379], - [5.8581, -0.2196, 14.3044], - [6.4772, -0.1025, 13.6183], - [7.8158, 0.0537, 12.6063], - [9.1664, 0.0507, 12.6302], - [10.5064, 0.0285, 12.8339], - [14.8361, 0.0818, 12.273], - [19.3933, 0.0801, 12.2991], - [23.9242, 0.0487, 12.9079], - [28.4922, 0.0379, 13.1666], - [36.145, 0.0277, 13.4572], - [43.7972, 0.0184, 13.7916], - [51.5125, 0.0154, 13.9248], - [59.2467, 0.0121, 14.0931], - [66.9428, 0.0084, 14.3151], - [74.6853, 0.0079, 14.3498], - [82.3722, 0.0052, 14.5512], - [90.1106, 0.0054, 14.5333], - [97.8369, 0.0043, 14.6288], - [105.6153, 0.0046, 14.5983], - [113.3686, 0.0036, 14.7076], - [121.1108, 0.003, 14.7785], - [136.61, 0.0026, 14.826], - [152.0708, 0.0018, 14.9298], - [167.6433, 0.0021, 14.8827], - [183.1011, 0.0012, 15.0438], - [198.5845, 0.0011, 15.0538], - [214.0264, 0.0008, 15.123] + [1.39875, 4.681865, 0.866627], + [2.5225, 2.326382, 4.161359], + [3.625, 1.361424, 6.595466], + [4.69125, 0.848354, 8.455342], + [5.705, 0.519685, 9.997214], + [6.70625, 0.36981, 10.852249], + [7.69375, 0.267029, 11.541523], + [8.67875, 0.210129, 11.979299], + [47.05, 0.030309, 13.539909], + [95.24375, 0.003774, 14.78837], + [211.0225, 0.000928, 15.059476] ] } }, @@ -246,94 +126,46 @@ "aspirate": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, "dispense": { "default": { "1": [ - [0.7511, 3.9556, 6.455], - [1.3075, 2.1664, 5.8839], - [1.8737, 1.1513, 7.2111], - [3.177, 0.9374, 7.612], - [4.5368, 0.5531, 8.8328], - [7.3103, 0.3035, 9.9651], - [10.0825, 0.1513, 11.0781], - [12.9776, 0.1293, 11.2991], - [15.9173, 0.0976, 11.7115], - [18.8243, 0.0624, 12.2706], - [21.8529, 0.07, 12.1275], - [24.8068, 0.0418, 12.7442], - [27.7744, 0.0356, 12.8984], - [35.2873, 0.0303, 13.0454], - [42.7989, 0.0202, 13.4038], - [50.4562, 0.0196, 13.4293], - [58.1081, 0.0145, 13.6843], - [65.7267, 0.0104, 13.9252], - [73.2857, 0.0068, 14.1606], - [81.0016, 0.0091, 13.9883], - [88.6617, 0.0064, 14.2052], - [103.9829, 0.0051, 14.3271], - [119.4408, 0.0049, 14.3475], - [134.889, 0.0037, 14.485], - [150.273, 0.0026, 14.6402], - [181.2798, 0.0026, 14.6427], - [212.4724, 0.0022, 14.7002], - [243.577, 0.0015, 14.8558], - [274.7216, 0.0012, 14.9205], - [305.8132, 0.0009, 15.0118], - [368.0697, 0.0007, 15.0668], - [430.2513, 0.0005, 15.1594], - [492.3487, 0.0003, 15.2291], - [554.5713, 0.0003, 15.2367], - [616.6825, 0.0002, 15.2949], - [694.4168, 0.0002, 15.3027], - [772.0327, 0.0001, 15.3494], - [849.617, 0.0001, 15.3717], - [927.2556, 0.0001, 15.3745], - [1004.87, 0.0001, 15.3912], - [1051.4648, 0.0001, 15.391] + [3.76, 2.041301, 4.284751], + [5.684286, 0.49624, 10.09418], + [8.445714, 0.187358, 11.849952], + [12.981429, 0.073135, 12.814653], + [17.667143, 0.060853, 12.974083], + [46.515714, 0.025888, 13.591828], + [95.032857, 0.006561, 14.490827], + [114.488571, 0.00306, 14.823556], + [192.228571, 0.001447, 15.00822], + [309.921429, 0.000995, 15.095087], + [436.984286, 0.000322, 15.303634], + [632.861429, 0.000208, 15.353582], + [828.952857, 0.00013, 15.402544], + [976.118571, 0.000095, 15.431673], + [1005.275714, -0.000067, 15.589843], + [1024.768571, -0.000021, 15.543681], + [1049.145714, -0.000013, 15.535884] ] } }, diff --git a/shared-data/pipette/fixtures/name/index.ts b/shared-data/pipette/fixtures/name/index.ts index 1625636981e..0831ffc7fdd 100644 --- a/shared-data/pipette/fixtures/name/index.ts +++ b/shared-data/pipette/fixtures/name/index.ts @@ -16,3 +16,4 @@ export const fixtureP300Multi: PipetteNameSpecs = pipetteNameSpecFixtures.p300_multi export const fixtureP1000Single: PipetteNameSpecs = pipetteNameSpecFixtures.p1000_single +export const fixtureP100096: PipetteNameSpecs = pipetteNameSpecFixtures.p1000_96 diff --git a/shared-data/pipette/schemas/2/pipetteGeometrySchema.json b/shared-data/pipette/schemas/2/pipetteGeometrySchema.json index df75f98ef08..4dd243304f9 100644 --- a/shared-data/pipette/schemas/2/pipetteGeometrySchema.json +++ b/shared-data/pipette/schemas/2/pipetteGeometrySchema.json @@ -7,6 +7,20 @@ "items": { "type": "number" }, "minItems": 3, "maxItems": 3 + }, + "nozzleName": { + "type": "string", + "pattern": "[A-Z]+[0-9]+" + }, + "rowName": { + "type": "string", + "pattern": "[A-Z]+", + "description": "The name of a row of nozzles" + }, + "columnName": { + "type": "string", + "pattern": "[0-9]+", + "description": "The name of a column of nozzles" } }, "type": "object", @@ -23,6 +37,38 @@ "type": "string", "pattern": "^pipette/definitions/[2]/geometry/([a-z]*_[a-z]*)+/p[0-9]{2,4}/[a-z]*[.]gltf" }, + "orderedRows": { + "type": "array", + "items": { + "type": "object", + "description": "A row of nozzle keys", + "required": ["key", "orderedNozzles"], + "properties": { + "key": { "$ref": "#/definitions/rowName" }, + "orderedNozzles": { + "type": "array", + "description": "The list of nozzle names in this row", + "items": { "$ref": "#/definitions/nozzleName" } + } + } + } + }, + "orderedColumns": { + "type": "array", + "items": { + "type": "object", + "description": "A column of nozzle keys", + "required": ["key", "orderedNozzles"], + "properties": { + "key": { "$ref": "#/definitions/columnName" }, + "orderedNozzles": { + "type": "array", + "description": "The list of nozzle names in this column", + "items": { "$ref": "#/definitions/nozzleName" } + } + } + } + }, "nozzleMap": { "type": "object", "description": "Unordered object of well objects with position and dimensional information", diff --git a/shared-data/pipette/schemas/2/pipettePropertiesSchema.json b/shared-data/pipette/schemas/2/pipettePropertiesSchema.json index 94c6482f155..b45870167c5 100644 --- a/shared-data/pipette/schemas/2/pipettePropertiesSchema.json +++ b/shared-data/pipette/schemas/2/pipettePropertiesSchema.json @@ -1,13 +1,13 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "opentronsPipetteGeometrySchemaV2", + "$id": "opentronsPipettePropertiesSchemaV2", "definitions": { "channels": { "enum": [1, 8, 96, 384] }, "displayCategory": { "type": "string", - "enum": ["GEN1"] + "enum": ["GEN1", "GEN2", "FLEX"] }, "positiveNumber": { "type": "number", @@ -18,33 +18,6 @@ "minimum": 0.01, "maximum": 2.5 }, - "xyzArray": { - "type": "array", - "description": "Array of 3 numbers, [x, y, z]", - "items": { "type": "number" }, - "minItems": 3, - "maxItems": 3 - }, - "linearEquations": { - "description": "Array containing any number of 3-arrays. Each inner 3-array describes a line segment: [boundary, slope, intercept]. So [1, 2, 3] would mean 'where (next_boundary > x >= 1), y = 2x + 3'", - "type": "array", - "items": { - "type": "array", - "items": { "type": "number" }, - "minItems": 3, - "maxItems": 3 - } - }, - "liquidHandlingSpecs": { - "description": "Object containing linear equations for translating between uL of liquid and mm of plunger travel. There is one linear equation for aspiration and one for dispense", - "type": "object", - "required": ["aspirate", "dispense"], - "additionalProperties": false, - "properties": { - "aspirate": { "$ref": "#/definitions/linearEquations" }, - "dispense": { "$ref": "#/definitions/linearEquations" } - } - }, "editConfigurations": { "type": "object", "description": "Object allowing you to modify a config", @@ -58,16 +31,14 @@ "displayName": { "type": "string" } } }, - "tipConfigurations": { + "partialTipCount": { + "type": "number", + "enum": [1, 2, 3, 4, 5, 6, 7, 8, 12, 96, 384] + }, + "currentByTipCount": { "type": "object", - "description": "Object containing configurations specific to tip handling", - "required": ["current", "speed"], - "properties": { - "current": { "$ref": "#/definitions/currentRange" }, - "presses": {}, - "speed": { "$ref": "#/definitions/editConfigurations" }, - "increment": {}, - "distance": {} + "patternProperties": { + "\\d+": { "$ref": "#/definitions/currentRange" } } } }, @@ -104,20 +75,29 @@ }, "channels": { "$ref": "#/definitions/channels" }, "partialTipConfigurations": { - "type": "object", "description": "Object containing information on partial tip configurations", - "required": ["partialTipSupported"], - "properties": { - "partialTipSupported": { "type": "boolean" }, - "availableConfigurations": { - "type": "array", - "description": "Array of available configurations", - "items": { - "type": "number", - "enum": [1, 2, 3, 4, 5, 6, 7, 8, 12, 96, 384] + "oneof": [ + { + "type": "object", + "required": ["partialTipSupported"], + "properties": { + "partialTipSupported": { "const": false }, + "availableConfigurations": null + } + }, + { + "type": "object", + "required": ["partialTipSupported", "availableConfigurations"], + "properties": { + "partialTipSupported": { "const": true }, + "availableConfigurations": { + "type": "array", + "description": "Array of available configurations", + "items": { "$ref": "#/definitions/partialTipCount" } + } } } - } + ] }, "availableSensors": { "type": "object", @@ -143,13 +123,20 @@ }, "plungerPositionsConfigurations": { "type": "object", - "description": "Object containing configurations specific to tip handling", - "required": ["top", "bottom", "blowout", "drop"], - "properties": { - "top": { "$ref": "#/definitions/currentRange" }, - "bottom": {}, - "blowout": { "$ref": "#/definitions/editConfigurations" }, - "drop": {} + "description": "Key positions of the plunger, by liquid configuration", + "required": ["default"], + "patternProperties": { + "\\w+": { + "type": "object", + "description": "Plunger positions for this liquid configuration", + "required": ["top", "bottom", "blowout", "drop"], + "properties": { + "top": { "type": "number" }, + "bottom": { "type": "number" }, + "blowout": { "type": "number" }, + "drop": { "type": "number" } + } + } } }, "plungerMotorConfigurations": { @@ -171,25 +158,124 @@ } }, "pickUpTipConfigurations": { - "$ref": "#/definitions/tipConfigurations" + "description": "Object containing configurations for picking up tips common to all partial configurations", + "anyOf": [ + { + "type": "object", + "required": ["pressFit"], + "properties": { + "pressFit": { + "type": "object", + "required": [ + "presses", + "speed", + "increment", + "distance", + "currentByTipCount" + ], + "additionalProperties": false, + "properties": { + "presses": { "$ref": "#/definitions/positiveNumber" }, + "speed": { "$ref": "#/definitions/positiveNumber" }, + "increment": { "$ref": "#/definitions/positiveNumber" }, + "distance": { "$ref": "#/definitions/positiveNumber" }, + "currentByTipCount": { + "$ref": "#/definitions/currentByTipCount" + } + } + } + } + }, + { + "type": "object", + "required": ["camAction"], + "properties": { + "camAction": { + "type": "object", + "required": [ + "prep_move_distance", + "prep_move_speed", + "speed", + "distance", + "currentByTipCount" + ], + "additionalProperties": false, + "properties": { + "prep_move_distance": { + "$ref": "#/definitions/positiveNumber" + }, + "prep_move_speed": { "$ref": "#/definitions/positiveNumber" }, + "speed": { "$ref": "#/definitions/positiveNumber" }, + "distance": { "$ref": "#/definitions/positiveNumber" }, + "currentByTipCount": { + "$ref": "#/definitions/currentByTipCount" + }, + "connectTiprackDistanceMM": { + "$ref": "#/definitions/positiveNumber" + } + } + } + } + } + ] }, "dropTipConfigurations": { - "$ref": "#/definitions/tipConfigurations" + "type": "object", + "description": "Object containing configurations specific to dropping tips", + "anyOf": [ + { + "type": "object", + "required": ["plungerEject"], + "properties": { + "plungerEject": { + "type": "object", + "required": ["current", "speed"], + "additionalProperties": false, + "properties": { + "current": { "$ref": "#/definitions/currentRange" }, + "speed": { "$ref": "#/definitions/positiveNumber" } + } + } + } + }, + { + "type": "object", + "required": ["camAction"], + "properties": { + "camAction": { + "type": "object", + "required": [ + "current", + "prep_move_distance", + "prep_move_speed", + "distance", + "speed" + ], + "additionalProperties": false, + "properties": { + "current": { "$ref": "#/definitions/currentRange" }, + "prep_move_distance": { + "$ref": "#/definitions/positiveNumber" + }, + "prep_move_speed": { "$ref": "#/definitions/positiveNumber" }, + "distance": { "$ref": "#/definitions/positiveNumber" }, + "speed": { "$ref": "#/definitions/positiveNumber" } + } + } + } + } + ] }, "displayName": { "type": "string", "description": "Display name of the pipette include model and generation number in readable format." }, "tipPresenceCheckDistanceMM": { - "type": "string", + "$ref": "#/definitions/positiveNumber", "description": "The distance to move the gear motors down to check tip presence status." }, - "connectTiprackDistanceMM": { - "type": "string", - "description": "The distance to move the z stage down to connect with the tiprack before clamping." - }, "endTipActionRetractDistanceMM": { - "type": "string", + "$ref": "#/definitions/positiveNumber", "description": "The distance to move the z stage up after a tip pickup or dropoff." }, "model": { diff --git a/shared-data/protocol/types/schemaV7/command/setup.ts b/shared-data/protocol/types/schemaV7/command/setup.ts index f0d3ff0b0da..e6048ef58c9 100644 --- a/shared-data/protocol/types/schemaV7/command/setup.ts +++ b/shared-data/protocol/types/schemaV7/command/setup.ts @@ -5,7 +5,6 @@ import type { LabwareOffset, PipetteName, ModuleModel, - FixtureLoadName, Cutout, } from '../../../../js' @@ -154,6 +153,6 @@ interface LoadLiquidResult { interface LoadFixtureParams { location: { cutout: Cutout } - loadName: FixtureLoadName + loadName: string fixtureId?: string } diff --git a/shared-data/python/opentrons_shared_data/deck/__init__.py b/shared-data/python/opentrons_shared_data/deck/__init__.py index 1c8d140e762..e922d905ec2 100644 --- a/shared-data/python/opentrons_shared_data/deck/__init__.py +++ b/shared-data/python/opentrons_shared_data/deck/__init__.py @@ -1,11 +1,11 @@ """ opentrons_shared_data.deck: types and bindings for deck definitions """ -from typing import Dict, NamedTuple, cast, overload, TYPE_CHECKING +from typing import Dict, List, NamedTuple, cast, overload, TYPE_CHECKING from typing_extensions import Final import json -from .. import load_shared_data +from .. import get_shared_data_root, load_shared_data if TYPE_CHECKING: from .dev_types import ( @@ -60,6 +60,14 @@ def load_schema(version: int) -> "DeckSchema": ) +def list_names(version: int) -> List[str]: + """Return all loadable deck definition names, for the given schema version.""" + definitions_directory = ( + get_shared_data_root() / "deck" / "definitions" / f"{version}" + ) + return [file.stem for file in definitions_directory.iterdir()] + + def get_calibration_square_position_in_slot(slot: int) -> Offset: """Get the position of an OT-3 deck slot's calibration square. diff --git a/shared-data/python/opentrons_shared_data/errors/codes.py b/shared-data/python/opentrons_shared_data/errors/codes.py index 1f104052cc8..ab79e03b06e 100644 --- a/shared-data/python/opentrons_shared_data/errors/codes.py +++ b/shared-data/python/opentrons_shared_data/errors/codes.py @@ -83,6 +83,7 @@ class ErrorCodes(Enum): COMMAND_PRECONDITION_VIOLATED = _code_from_dict_entry("4004") COMMAND_PARAMETER_LIMIT_VIOLATED = _code_from_dict_entry("4005") INVALID_PROTOCOL_DATA = _code_from_dict_entry("4006") + API_MISCONFIGURATION = _code_from_dict_entry("4007") @classmethod @lru_cache(25) diff --git a/shared-data/python/opentrons_shared_data/errors/exceptions.py b/shared-data/python/opentrons_shared_data/errors/exceptions.py index fa784e5fb90..9483b404965 100644 --- a/shared-data/python/opentrons_shared_data/errors/exceptions.py +++ b/shared-data/python/opentrons_shared_data/errors/exceptions.py @@ -19,7 +19,7 @@ def __init__( self, code: ErrorCodes, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence["EnumeratedError"]] = None, ) -> None: """Build an EnumeratedError.""" @@ -61,7 +61,7 @@ def __init__( self, code: Optional[ErrorCodes] = None, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a CommunicationError.""" @@ -88,7 +88,7 @@ def __init__( self, code: Optional[ErrorCodes] = None, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a RoboticsControlError.""" @@ -116,7 +116,7 @@ def __init__( self, code: Optional[ErrorCodes] = None, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a RoboticsInteractionError.""" @@ -144,7 +144,7 @@ def __init__( self, code: Optional[ErrorCodes] = None, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[Union[EnumeratedError, BaseException]]] = None, ) -> None: """Build a GeneralError.""" @@ -220,7 +220,7 @@ class RobotInUseError(CommunicationError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a CanbusCommunicationError.""" @@ -233,7 +233,7 @@ class CanbusCommunicationError(CommunicationError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a CanbusCommunicationError.""" @@ -248,7 +248,7 @@ class InternalUSBCommunicationError(CommunicationError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an InternalUSBCommunicationError.""" @@ -263,7 +263,7 @@ class ModuleCommunicationError(CommunicationError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a CanbusCommunicationError.""" @@ -278,7 +278,7 @@ class CommandTimedOutError(CommunicationError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a CommandTimedOutError.""" @@ -291,7 +291,7 @@ class FirmwareUpdateFailedError(CommunicationError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a FirmwareUpdateFailedError.""" @@ -304,7 +304,7 @@ class InternalMessageFormatError(CommunicationError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an InternalMesasgeFormatError.""" @@ -319,7 +319,7 @@ class CANBusConfigurationError(CommunicationError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a CANBus Configuration Error.""" @@ -334,7 +334,7 @@ class CANBusBusError(CommunicationError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a CANBus Bus Error.""" @@ -347,7 +347,7 @@ class MotionFailedError(RoboticsControlError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a MotionFailedError.""" @@ -360,7 +360,7 @@ class HomingFailedError(RoboticsControlError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a HomingFailedError.""" @@ -373,7 +373,7 @@ class StallOrCollisionDetectedError(RoboticsControlError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a StallOrCollisionDetectedError.""" @@ -388,7 +388,7 @@ class MotionPlanningFailureError(RoboticsControlError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a MotionPlanningFailureError.""" @@ -401,7 +401,7 @@ class PositionEstimationInvalidError(RoboticsControlError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a PositionEstimationFailedError.""" @@ -416,7 +416,7 @@ class MoveConditionNotMetError(RoboticsControlError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a MoveConditionNotMetError.""" @@ -435,7 +435,7 @@ def __init__( self, structure_height: float, lower_limit: float, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a CalibrationStructureNotFoundError.""" @@ -454,7 +454,7 @@ def __init__( self, edge_name: str, stride: float, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a EdgeNotFoundError.""" @@ -473,7 +473,7 @@ def __init__( self, found: float, nominal: float, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a EarlyCapacitiveSenseTrigger.""" @@ -492,7 +492,7 @@ def __init__( self, found: float, nominal: float, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a InaccurateNonContactSweepError.""" @@ -513,7 +513,7 @@ class MisalignedGantryError(RoboticsControlError): def __init__( self, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a MisalignedGantryError.""" @@ -534,7 +534,7 @@ class UnmatchedTipPresenceStates(RoboticsControlError): def __init__( self, states: Dict[int, int], - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an UnmatchedTipPresenceStatesError.""" @@ -562,7 +562,7 @@ class PositionUnknownError(RoboticsControlError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a PositionUnknownError.""" @@ -575,7 +575,7 @@ class ExecutionCancelledError(RoboticsControlError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a ExecutionCancelledError.""" @@ -588,7 +588,7 @@ class LabwareDroppedError(RoboticsInteractionError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a LabwareDroppedError.""" @@ -601,7 +601,7 @@ class TipPickupFailedError(RoboticsInteractionError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a TipPickupFailedError.""" @@ -614,7 +614,7 @@ class TipDropFailedError(RoboticsInteractionError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a TipPickupFailedError.""" @@ -629,7 +629,7 @@ def __init__( action: str, pipette_name: str, mount: str, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an UnexpectedTipRemovalError.""" @@ -651,7 +651,7 @@ def __init__( pipette_name: str, mount: str, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an UnexpectedTipAttachError.""" @@ -670,7 +670,7 @@ def __init__( action: str, subsystems_to_update: List[Any], message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a FirmwareUpdateRequiredError.""" @@ -687,7 +687,7 @@ class PipetteOverpressureError(RoboticsInteractionError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an PipetteOverpressureError.""" @@ -700,7 +700,7 @@ class EStopActivatedError(RoboticsInteractionError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an EStopActivatedError.""" @@ -713,7 +713,7 @@ class EStopNotPresentError(RoboticsInteractionError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an EStopNotPresentError.""" @@ -726,7 +726,7 @@ class PipetteNotPresentError(RoboticsInteractionError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an PipetteNotPresentError.""" @@ -739,7 +739,7 @@ class GripperNotPresentError(RoboticsInteractionError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a GripperNotPresentError.""" @@ -752,7 +752,7 @@ class InvalidActuator(RoboticsInteractionError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an InvalidActuator.""" @@ -766,7 +766,7 @@ def __init__( self, identifier: str, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a ModuleNotPresentError.""" @@ -784,7 +784,7 @@ class InvalidInstrumentData(RoboticsInteractionError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an InvalidInstrumentData.""" @@ -797,7 +797,7 @@ class InvalidLiquidClassName(RoboticsInteractionError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an InvalidLiquidClassName.""" @@ -812,7 +812,7 @@ class TipDetectorNotFound(RoboticsInteractionError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a TipDetectorNotFound.""" @@ -827,7 +827,7 @@ def __init__( api_element: str, since_version: str, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an APIRemoved error.""" @@ -849,7 +849,7 @@ class CommandPreconditionViolated(GeneralError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build a CommandPreconditionViolated instance.""" @@ -893,7 +893,7 @@ class UnsupportedHardwareCommand(GeneralError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an UnsupportedHardwareCommand.""" @@ -908,7 +908,7 @@ class InvalidProtocolData(GeneralError): def __init__( self, message: Optional[str] = None, - detail: Optional[Dict[str, Any]] = None, + detail: Optional[Dict[str, str]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: """Build an InvalidProtocolData.""" diff --git a/shared-data/python/opentrons_shared_data/pipette/load_data.py b/shared-data/python/opentrons_shared_data/pipette/load_data.py index abf5fa1c2b0..4dc6b200574 100644 --- a/shared-data/python/opentrons_shared_data/pipette/load_data.py +++ b/shared-data/python/opentrons_shared_data/pipette/load_data.py @@ -1,7 +1,7 @@ import json import os -from typing import Dict, Any, Union, Optional +from typing import Dict, Any, Union, Optional, List from typing_extensions import Literal from functools import lru_cache @@ -152,7 +152,41 @@ def _change_to_camel_case(c: str) -> str: return f"{config_name[0]}" + "".join(s.capitalize() for s in config_name[1::]) -def update_pipette_configuration( # noqa: C901 +def _edit_non_quirk_with_lc_override( + mutable_config_key: str, + new_mutable_value: Any, + base_dict: Dict[str, Any], + liquid_class: Optional[LiquidClasses], +) -> None: + def _do_edit_non_quirk( + new_value: Any, existing: Dict[Any, Any], keypath: List[Any] + ) -> None: + thiskey: Any = keypath[0] + if thiskey in [lc.name for lc in LiquidClasses]: + if liquid_class: + thiskey = liquid_class + else: + thiskey = LiquidClasses[thiskey] + if len(keypath) > 1: + restkeys = keypath[1:] + if thiskey == "##EACHTIP##": + for key in existing.keys(): + _do_edit_non_quirk(new_value, existing[key], restkeys) + else: + _do_edit_non_quirk(new_value, existing[thiskey], restkeys) + else: + # This was the last key + if thiskey == "##EACHTIP##": + for key in existing.keys(): + existing[key] = new_value + else: + existing[thiskey] = new_value + + new_names = _MAP_KEY_TO_V2[mutable_config_key] + _do_edit_non_quirk(new_mutable_value, base_dict, new_names) + + +def update_pipette_configuration( base_configurations: PipetteConfigurations, v1_configuration_changes: Dict[str, Any], liquid_class: Optional[LiquidClasses] = None, @@ -169,40 +203,11 @@ def update_pipette_configuration( # noqa: C901 lookup_key = _change_to_camel_case(c) if c == "quirks" and isinstance(v, dict): quirks_list.extend([b.name for b in v.values() if b.value]) - elif liquid_class: - if lookup_key == "tipLength": - new_names = _MAP_KEY_TO_V2[lookup_key] - top_name = new_names["top_level_name"] - nested_name = new_names["nested_name"] - # This is only a concern for OT-2 configs and I think we can - # be less smart about handling multiple tip types by updating - # all tips. - for k in dict_of_base_model["liquid_properties"][liquid_class][ - new_names["top_level_name"] - ].keys(): - dict_of_base_model["liquid_properties"][liquid_class][top_name][k][ - nested_name - ] = v - else: - dict_of_base_model["liquid_properties"][liquid_class].pop(lookup_key) - dict_of_base_model["liquid_properties"][liquid_class][lookup_key] = v else: - try: - dict_of_base_model.pop(lookup_key) - dict_of_base_model[lookup_key] = v - except KeyError: - # The name is not the same format as previous so - # we need to look it up from the V2 key map - new_names = _MAP_KEY_TO_V2[lookup_key] - top_name = new_names["top_level_name"] - nested_name = new_names["nested_name"] - if new_names.get("liquid_class"): - # isinstances are needed for type checking. - liquid_class = LiquidClasses[new_names["liquid_class"]] - dict_of_base_model[top_name][liquid_class][nested_name] = v - else: - # isinstances are needed for type checking. - dict_of_base_model[top_name][nested_name] = v + _edit_non_quirk_with_lc_override( + lookup_key, v, dict_of_base_model, liquid_class + ) + dict_of_base_model["quirks"] = list( set(dict_of_base_model["quirks"]) - set(quirks_list) ) diff --git a/shared-data/python/opentrons_shared_data/pipette/model_constants.py b/shared-data/python/opentrons_shared_data/pipette/model_constants.py index d57ffa587ab..ea79ecd1154 100644 --- a/shared-data/python/opentrons_shared_data/pipette/model_constants.py +++ b/shared-data/python/opentrons_shared_data/pipette/model_constants.py @@ -1,4 +1,4 @@ -from typing import Dict, Union +from typing import Dict, Union, List from .types import ( Quirks, @@ -54,57 +54,29 @@ RESTRICTED_MUTABLE_CONFIG_KEYS = [*VALID_QUIRKS, "model"] -_MAP_KEY_TO_V2: Dict[str, Dict[str, str]] = { - "top": { - "top_level_name": "plungerPositionsConfigurations", - "nested_name": "top", - "liquid_class": "default", - }, - "bottom": { - "top_level_name": "plungerPositionsConfigurations", - "nested_name": "bottom", - "liquid_class": "default", - }, - "blowout": { - "top_level_name": "plungerPositionsConfigurations", - "nested_name": "blowout", - "liquid_class": "default", - }, - "dropTip": { - "top_level_name": "plungerPositionsConfigurations", - "nested_name": "drop", - "liquid_class": "default", - }, - "pickUpCurrent": { - "top_level_name": "pickUpTipConfigurations", - "nested_name": "current", - }, - "pickUpDistance": { - "top_level_name": "pickUpTipConfigurations", - "nested_name": "distance", - }, - "pickUpIncrement": { - "top_level_name": "pickUpTipConfigurations", - "nested_name": "increment", - }, - "pickUpPresses": { - "top_level_name": "pickUpTipConfigurations", - "nested_name": "presses", - }, - "pickUpSpeed": { - "top_level_name": "pickUpTipConfigurations", - "nested_name": "speed", - }, - "plungerCurrent": { - "top_level_name": "plungerMotorConfigurations", - "nested_name": "run", - }, - "dropTipCurrent": { - "top_level_name": "dropTipConfigurations", - "nested_name": "current", - }, - "dropTipSpeed": {"top_level_name": "dropTipConfigurations", "nested_name": "speed"}, - "tipLength": {"top_level_name": "supportedTips", "nested_name": "defaultTipLength"}, + +_MAP_KEY_TO_V2: Dict[str, List[str]] = { + "top": ["plungerPositionsConfigurations", "default", "top"], + "bottom": ["plungerPositionsConfigurations", "default", "bottom"], + "blowout": ["plungerPositionsConfigurations", "default", "blowout"], + "dropTip": ["plungerPositionsConfigurations", "default", "drop"], + "pickUpCurrent": ["pickUpTipConfigurations", "pressFit", "currentByTipCount"], + "pickUpDistance": ["pickUpTipConfigurations", "pressFit", "distance"], + "pickUpIncrement": ["pickUpTipConfigurations", "pressFit", "increment"], + "pickUpPresses": ["pickUpTipConfigurations", "pressFit", "presses"], + "pickUpSpeed": ["pickUpTipConfigurations", "pressFit", "speed"], + "plungerCurrent": ["plungerMotorConfigurations", "run"], + "dropTipCurrent": ["dropTipConfigurations", "plungerEject", "current"], + "dropTipSpeed": ["dropTipConfigurations", "plungerEject", "speed"], + "maxVolume": ["liquid_properties", "default", "maxVolume"], + "minVolume": ["liquid_properties", "default", "minVolume"], + "tipLength": [ + "liquid_properties", + "default", + "supportedTips", + "##EACHTIP##", + "defaultTipLength", + ], } diff --git a/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py b/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py index d77af0620d8..93d4b4c7d53 100644 --- a/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py +++ b/shared-data/python/opentrons_shared_data/pipette/mutable_configurations.py @@ -40,6 +40,34 @@ LIQUID_CLASS = LiquidClasses.default +def _edit_non_quirk( + mutable_config_key: str, new_mutable_value: MutableConfig, base_dict: Dict[str, Any] +) -> None: + def _do_edit_non_quirk( + new_value: MutableConfig, existing: Dict[Any, Any], keypath: List[Any] + ) -> None: + thiskey: Any = keypath[0] + if thiskey in [lc.name for lc in LiquidClasses]: + thiskey = LiquidClasses[thiskey] + if len(keypath) > 1: + restkeys = keypath[1:] + if thiskey == "##EACHTIP##": + for key in existing.keys(): + _do_edit_non_quirk(new_value, existing[key], restkeys) + else: + _do_edit_non_quirk(new_value, existing[thiskey], restkeys) + else: + # This was the last key + if thiskey == "##EACHTIP##": + for key in existing.keys(): + existing[key] = new_value.value + else: + existing[thiskey] = new_value.value + + new_names = _MAP_KEY_TO_V2[mutable_config_key] + _do_edit_non_quirk(new_mutable_value, base_dict, new_names) + + def _migrate_to_v2_configurations( base_configurations: PipetteConfigurations, v1_mutable_configs: OverrideType, @@ -59,26 +87,9 @@ def _migrate_to_v2_configurations( continue if c == "quirks" and isinstance(v, dict): quirks_list.extend([b.name for b in v.values() if b.value]) - else: - new_names = _MAP_KEY_TO_V2[c] - top_name = new_names["top_level_name"] - nested_name = new_names["nested_name"] - if c == "tipLength" and isinstance(v, MutableConfig): - # This is only a concern for OT-2 configs and I think we can - # be less smart about handling multiple tip types by updating - # all tips. - for k in dict_of_base_model["liquid_properties"][LIQUID_CLASS][ - new_names["top_level_name"] - ].keys(): - dict_of_base_model["liquid_properties"][LIQUID_CLASS][top_name][k][ - nested_name - ] = v - elif new_names.get("liquid_class") and isinstance(v, MutableConfig): - _class = LiquidClasses[new_names["liquid_class"]] - dict_of_base_model[top_name][_class][nested_name] = v.value - elif isinstance(v, MutableConfig): - # isinstances are needed for type checking. - dict_of_base_model[top_name][nested_name] = v.value + elif isinstance(v, MutableConfig): + _edit_non_quirk(c, v, dict_of_base_model) + dict_of_base_model["quirks"] = list( set(dict_of_base_model["quirks"]).union(set(quirks_list)) ) @@ -143,32 +154,50 @@ def _list_all_mutable_configs( return default_configurations +def _get_default_value_for(config: Dict[str, Any], keypath: List[str]) -> Any: + def _do_get_default_value_for( + remaining_config: Dict[Any, Any], keypath: List[str] + ) -> Any: + first: Any = keypath[0] + if first in [lc.name for lc in LiquidClasses]: + first = LiquidClasses[first] + if len(keypath) > 1: + rest = keypath[1:] + if first == "##EACHTIP##": + tip_list = list(remaining_config.keys()) + tip_list.sort(key=lambda o: o.value) + return _do_get_default_value_for(remaining_config[tip_list[-1]], rest) + else: + return _do_get_default_value_for(remaining_config[first], rest) + else: + if first == "###EACHTIP##": + tip_list = list(remaining_config.keys()) + tip_list.sort(key=lambda o: o.value) + return remaining_config[tip_list[-1]] + elif first == "currentByTipCount": + # return the value for the most tips at a time + cbt = remaining_config[first] + return cbt[next(reversed(sorted(cbt.keys())))] + else: + return remaining_config[first] + + return _do_get_default_value_for(config, keypath) + + def _find_default(name: str, configs: Dict[str, Any]) -> MutableConfig: """Find the default value from the configs and return it as a mutable config.""" - lookup_dict = _MAP_KEY_TO_V2[name] - nested_name = lookup_dict["nested_name"] - - min_max_dict = _MIN_MAX_LOOKUP[nested_name] - type_lookup = _TYPE_LOOKUP[nested_name] - units_lookup = _UNITS_LOOKUP[nested_name] - if name == "tipLength": - # This is only a concern for OT-2 configs and I think we can - # be less smart about handling multiple tip types. Instead, just - # get the max tip type. - tip_list = list( - configs["liquid_properties"][LIQUID_CLASS][ - lookup_dict["top_level_name"] - ].keys() - ) - tip_list.sort(key=lambda o: o.value) - default_value = configs["liquid_properties"][LIQUID_CLASS][ - lookup_dict["top_level_name"] - ][tip_list[-1]][nested_name] - elif lookup_dict.get("liquid_class"): - _class = LiquidClasses[lookup_dict["liquid_class"]] - default_value = configs[lookup_dict["top_level_name"]][_class][nested_name] + keypath = _MAP_KEY_TO_V2[name] + nested_name = keypath[-1] + + if name == "pickUpCurrent": + min_max_dict = _MIN_MAX_LOOKUP["current"] + type_lookup = _TYPE_LOOKUP["current"] + units_lookup = _UNITS_LOOKUP["current"] else: - default_value = configs[lookup_dict["top_level_name"]][nested_name] + min_max_dict = _MIN_MAX_LOOKUP[nested_name] + type_lookup = _TYPE_LOOKUP[nested_name] + units_lookup = _UNITS_LOOKUP[nested_name] + default_value = _get_default_value_for(configs, keypath) return MutableConfig( value=default_value, default=default_value, diff --git a/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py b/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py index 7445baddbcc..7374972b800 100644 --- a/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py +++ b/shared-data/python/opentrons_shared_data/pipette/pipette_definition.py @@ -1,5 +1,5 @@ import re -from typing import List, Dict, Tuple +from typing import List, Dict, Tuple, Optional from pydantic import BaseModel, Field, validator from typing_extensions import Literal from dataclasses import dataclass @@ -9,7 +9,9 @@ PLUNGER_CURRENT_MINIMUM = 0.1 PLUNGER_CURRENT_MAXIMUM = 1.5 -NOZZLE_MAP_NAMES = re.compile(r"[A-Z]{1}[0-9]{1,2}") +NOZZLE_MAP_NAMES = re.compile(r"(?P[A-Z]+)(?P[0-9]+)") +COLUMN_NAMES = re.compile(r"[0-9]+") +ROW_NAMES = re.compile(r"[A-Z]+") # TODO (lc 12-5-2022) Ideally we can deprecate this @@ -141,32 +143,101 @@ class PlungerPositions(BaseModel): class PlungerHomingConfigurations(BaseModel): current: float = Field( - ..., - description="Either the z motor current needed for picking up tip or the plunger motor current for dropping tip off the nozzle.", + default=0.0, + description="The current to move the plunger axis for homing.", ) speed: float = Field( ..., - description="The speed to move the z or plunger axis for tip pickup or drop off.", + description="The speed to move the plunger axis for homing.", ) -class TipHandlingConfigurations(PlungerHomingConfigurations): +class PressFitPickUpTipConfiguration(BaseModel): presses: int = Field( - default=0.0, description="The number of tries required to force pick up a tip." + ..., + description="The number of times to force pickup (incrementally more each time by increment)", ) increment: float = Field( - default=0.0, - description="The increment to move the pipette down for force tip pickup retries.", + ..., + description="The increment to move the pipette down on each force tip pickup press", ) distance: float = Field( - default=0.0, description="The distance to begin a pick up tip from." + ..., description="The starting distance to begin a pick up tip from" + ) + speed: float = Field( + ..., description="The speed to move the Z axis for each force pickup" + ) + current_by_tip_count: Dict[int, float] = Field( + ..., + description="A current dictionary look-up by partial tip configuration.", + alias="currentByTipCount", ) + + +class CamActionPickUpTipConfiguration(BaseModel): + distance: float = Field(..., description="How far to move the cams once engaged") + speed: float = Field(..., description="How fast to move the cams when engaged") prep_move_distance: float = Field( - default=0.0, - description="The distance to move downward before tip pickup or drop-off.", + ..., description="How far to move the cams to engage the rack" ) prep_move_speed: float = Field( - default=0.0, description="The speed for the optional preparatory move." + ..., description="How fast to move the cams when moving to the rack" + ) + current_by_tip_count: Dict[int, float] = Field( + ..., + description="A current dictionary look-up by partial tip configuration.", + alias="currentByTipCount", + ) + connect_tiprack_distance_mm: float = Field( + description="The distance to move the head down to connect with the tiprack before clamping.", + alias="connectTiprackDistanceMM", + ) + + +class PlungerEjectDropTipConfiguration(BaseModel): + current: float = Field( + ..., description="The current to use on the plunger motor when dropping a tip" + ) + speed: float = Field( + ..., description="How fast to move the plunger motor when dropping a tip" + ) + + +class CamActionDropTipConfiguration(BaseModel): + current: float = Field( + ..., description="The current to use on the cam motors when dropping tips" + ) + distance: float = Field( + ..., description="The distance to move the cams when dropping tips" + ) + speed: float = Field( + ..., description="How fast to move the cams when dropping tips" + ) + prep_move_distance: float = Field( + ..., description="How far to move the cams after disengaging" + ) + prep_move_speed: float = Field( + ..., description="How fast to move the cams after disengaging" + ) + + +class DropTipConfigurations(BaseModel): + plunger_eject: Optional[PlungerEjectDropTipConfiguration] = Field( + description="Configuration for tip drop via plunger eject", alias="plungerEject" + ) + cam_action: Optional[CamActionDropTipConfiguration] = Field( + description="Configuration for tip drop via cam action", alias="camAction" + ) + + +class PickUpTipConfigurations(BaseModel): + press_fit: PressFitPickUpTipConfiguration = Field( + description="Configuration for tip pickup via press fit", alias="pressFit" + ) + cam_action: Optional[CamActionPickUpTipConfiguration] = Field( + default=None, + description="Configuration for tip pickup via cam action", + alias="camAction", ) @@ -187,11 +258,6 @@ class PartialTipDefinition(BaseModel): description="A list of the types of partial tip configurations supported, listed by channel ints", alias="availableConfigurations", ) - per_tip_pickup_current: float = Field( - default=0.5, - description="A current scale for pick up tip in a partial tip configuration", - alias="perTipPickupCurrent", - ) class PipettePhysicalPropertiesDefinition(BaseModel): @@ -215,10 +281,10 @@ class PipettePhysicalPropertiesDefinition(BaseModel): display_category: pip_types.PipetteGenerationType = Field( ..., description="The product model of the pipette.", alias="displayCategory" ) - pick_up_tip_configurations: TipHandlingConfigurations = Field( + pick_up_tip_configurations: PickUpTipConfigurations = Field( ..., alias="pickUpTipConfigurations" ) - drop_tip_configurations: TipHandlingConfigurations = Field( + drop_tip_configurations: DropTipConfigurations = Field( ..., alias="dropTipConfigurations" ) plunger_homing_configurations: PlungerHomingConfigurations = Field( @@ -258,14 +324,10 @@ class PipettePhysicalPropertiesDefinition(BaseModel): description="The distance the high throughput tip motors will travel to check tip status.", alias="tipPresenceCheckDistanceMM", ) - connect_tiprack_distance_mm: float = Field( - default=0, - description="The distance to move the head down to connect with the tiprack before clamping.", - alias="connectTiprackDistanceMM", - ) + end_tip_action_retract_distance_mm: float = Field( - default=0, - description="The distance to move the head up after a tip pickup or dropoff.", + default=0.0, + description="The distance to move the head up after a tip drop or pickup.", alias="endTipActionRetractDistanceMM", ) @@ -302,6 +364,28 @@ class Config: } +class PipetteRowDefinition(BaseModel): + key: str + ordered_nozzles: List[str] = Field(..., alias="orderedNozzles") + + @validator("key") + def check_key_is_row(cls, v: str) -> str: + if not ROW_NAMES.search(v): + raise ValueError(f"{v} is not a valid row name") + return v + + +class PipetteColumnDefinition(BaseModel): + key: str + ordered_nozzles: List[str] = Field(..., alias="orderedNozzles") + + @validator("key") + def check_key_is_column(cls, v: str) -> str: + if not COLUMN_NAMES.search(v): + raise ValueError(f"{v} is not a valid column name") + return v + + class PipetteGeometryDefinition(BaseModel): """The geometry properties definition of a pipette.""" @@ -312,6 +396,8 @@ class PipetteGeometryDefinition(BaseModel): alias="pathTo3D", ) nozzle_map: Dict[str, List[float]] = Field(..., alias="nozzleMap") + ordered_columns: List[PipetteColumnDefinition] = Field(..., alias="orderedColumns") + ordered_rows: List[PipetteRowDefinition] = Field(..., alias="orderedRows") @validator("nozzle_map", pre=True) def check_nonempty_strings( diff --git a/shared-data/python/opentrons_shared_data/pipette/scripts/build_json_script.py b/shared-data/python/opentrons_shared_data/pipette/scripts/build_json_script.py index 54dfe3d1060..a7af2e30911 100644 --- a/shared-data/python/opentrons_shared_data/pipette/scripts/build_json_script.py +++ b/shared-data/python/opentrons_shared_data/pipette/scripts/build_json_script.py @@ -12,12 +12,15 @@ PipetteGeometryDefinition, PipetteLiquidPropertiesDefinition, PipettePhysicalPropertiesDefinition, - TipHandlingConfigurations, PlungerPositions, SupportedTipsDefinition, MotorConfigurations, PartialTipDefinition, AvailableSensorDefinition, + PickUpTipConfigurations, + PressFitPickUpTipConfiguration, + DropTipConfigurations, + PlungerEjectDropTipConfiguration, ) from ..dev_types import PipetteModelSpec @@ -33,21 +36,20 @@ GEOMETRY_SCHEMA = "#/pipette/schemas/2/pipetteGeometryPropertiesSchema.json" -def _build_tip_handling_configurations( - tip_handling_type: str, model_configurations: Optional[PipetteModelSpec] = None -) -> TipHandlingConfigurations: +def _build_pickup_tip_data( + model_configurations: Optional[PipetteModelSpec] = None, +) -> PickUpTipConfigurations: presses = 0 increment = 0 distance = 0.0 - if tip_handling_type == "pickup" and model_configurations: + if model_configurations: current = model_configurations["pickUpCurrent"]["value"] speed = model_configurations["pickUpSpeed"]["value"] presses = model_configurations["pickUpPresses"]["value"] increment = int(model_configurations["pickUpIncrement"]["value"]) distance = model_configurations["pickUpDistance"]["value"] - elif tip_handling_type == "pickup": + else: print("Handling pick up tip configurations\n") - current = float(input("please provide the current\n")) speed = float(input("please provide the speed\n")) presses = int(input("please provide the number of presses for force pick up\n")) increment = int( @@ -58,19 +60,32 @@ def _build_tip_handling_configurations( distance = float( input("please provide the starting distance for pick up tip\n") ) - elif tip_handling_type == "drop" and model_configurations: + print(f"TODO: Current {current} is not used yet") + return PickUpTipConfigurations( + pressFit=PressFitPickUpTipConfiguration( + speed=speed, + presses=presses, + increment=increment, + distance=distance, + currentByTipCount={}, + ) + ) + + +def _build_drop_tip_data( + model_configurations: Optional[PipetteModelSpec] = None, +) -> DropTipConfigurations: + if model_configurations: current = model_configurations["dropTipCurrent"]["value"] speed = model_configurations["dropTipSpeed"]["value"] - elif tip_handling_type == "drop": + else: print("Handling drop tip configurations\n") - current = float(input("please provide the current\n")) speed = float(input("please provide the speed\n")) - return TipHandlingConfigurations( - current=current, - speed=speed, - presses=presses, - increment=increment, - distance=distance, + return DropTipConfigurations( + plungerEject=PlungerEjectDropTipConfiguration( + current=current, + speed=speed, + ) ) @@ -186,8 +201,8 @@ def build_physical_model_v2( shaft_ul_per_mm = float( input(f"Please provide the uL to mm conversion for {pipette_type}\n") ) - pick_up_tip_configurations = _build_tip_handling_configurations("pickup") - drop_tip_configurations = _build_tip_handling_configurations("drop") + pick_up_tip_configurations = _build_pickup_tip_data() + drop_tip_configurations = _build_drop_tip_data() plunger_positions = _build_plunger_positions() plunger_motor_configurations = _build_motor_configurations() partial_tip_configurations = _build_partial_tip_configurations(int(channels)) diff --git a/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py b/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py index 16c434d9897..220214cd1e7 100644 --- a/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py +++ b/shared-data/python/opentrons_shared_data/pipette/scripts/update_configuration_files.py @@ -145,7 +145,7 @@ def update( Recursively update the given dictionary to ensure no data is lost when updating. """ next_key = next(iter_of_configs, None) - if next_key and isinstance(dict_to_update[next_key], dict): + if next_key and isinstance(dict_to_update.get(next_key), dict): dict_to_update[next_key] = update( dict_to_update.get(next_key, {}), iter_of_configs, value_to_update ) @@ -157,8 +157,8 @@ def update( def build_nozzle_map( nozzle_offset: List[float], channels: PipetteChannelType ) -> Dict[str, List[float]]: - Y_OFFSET = 9 - X_OFFSET = -9 + Y_OFFSET = -9 + X_OFFSET = 9 if channels == PipetteChannelType.SINGLE_CHANNEL: return {"A1": nozzle_offset} elif channels == PipetteChannelType.EIGHT_CHANNEL: @@ -350,7 +350,7 @@ def _update_single_model(configuration_to_update: List[str]) -> None: def _update_all_models(configuration_to_update: List[str]) -> None: - paths_to_validate = ROOT / "liquid" + paths_to_validate = ROOT / "general" _channel_model_str = { "single_channel": "single", "ninety_six_channel": "96", @@ -378,7 +378,6 @@ def _update_all_models(configuration_to_update: List[str]) -> None: ) model_version = convert_pipette_model(built_model) - load_and_update_file_from_config( configuration_to_update, value_to_update, model_version ) diff --git a/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py b/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py index 4147afb1149..081fe604d95 100644 --- a/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py +++ b/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py @@ -63,6 +63,8 @@ class Params(BaseModel): namespace: Optional[str] version: Optional[int] pushOut: Optional[float] + # schema v8 add-ons + addressableAreaName: Optional[str] class Command(BaseModel): diff --git a/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py b/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py index 48446e15f1b..ac0d92f48b9 100644 --- a/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py +++ b/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py @@ -81,6 +81,7 @@ class Location(BaseModel): slotName: Optional[str] moduleId: Optional[str] labwareId: Optional[str] + addressableAreaName: Optional[str] class ProfileStep(BaseModel): diff --git a/shared-data/python/tests/deck/__init__.py b/shared-data/python/tests/deck/__init__.py index b5cc3452b34..e69de29bb2d 100644 --- a/shared-data/python/tests/deck/__init__.py +++ b/shared-data/python/tests/deck/__init__.py @@ -1,18 +0,0 @@ -from typing import List -from pathlib import Path - - -def list_deck_def_paths(version: int) -> List[str]: - loadnames = [ - deffile - for deffile in ( - Path(__file__).parent - / ".." - / ".." - / ".." - / "deck" - / "definitions" - / f"{version}" - ).iterdir() - ] - return [loadname.stem for loadname in loadnames] diff --git a/shared-data/python/tests/deck/test_list_names.py b/shared-data/python/tests/deck/test_list_names.py new file mode 100644 index 00000000000..02c895f0246 --- /dev/null +++ b/shared-data/python/tests/deck/test_list_names.py @@ -0,0 +1,12 @@ +import pytest + +from opentrons_shared_data.deck import list_names + + +@pytest.mark.parametrize("version", [3, 4]) +def test_list_names(version: int) -> None: + """Make sure `list_names()` returns something. + + Just a basic test to make sure it's not looking in a nonexistent directory or something. + """ + assert len(list_names(version)) > 0 diff --git a/shared-data/python/tests/deck/test_position.py b/shared-data/python/tests/deck/test_position.py index 13aefeb21b4..af9dff84b31 100644 --- a/shared-data/python/tests/deck/test_position.py +++ b/shared-data/python/tests/deck/test_position.py @@ -2,7 +2,10 @@ import pytest -from opentrons_shared_data.deck import load as load_deck_definition +from opentrons_shared_data.deck import ( + list_names as list_deck_definition_names, + load as load_deck_definition, +) from opentrons_shared_data.deck.dev_types import ( AddressableArea, Cutout, @@ -11,8 +14,6 @@ DeckDefinitionV4, ) -from . import list_deck_def_paths - def as_tuple(list: List[float]) -> Tuple[float, float, float]: """Convert an [x,y,z] list from the definitions to an (x,y,z) tuple, for hashability.""" @@ -91,7 +92,7 @@ def get_v4_slot_positions( return set(slot_positions) -@pytest.mark.parametrize("definition_name", list_deck_def_paths(version=4)) +@pytest.mark.parametrize("definition_name", list_deck_definition_names(version=4)) def test_v3_and_v4_positional_equivalence(definition_name: str) -> None: deck_v3 = load_deck_definition(name=definition_name, version=3) deck_v4 = load_deck_definition(name=definition_name, version=4) diff --git a/shared-data/python/tests/deck/test_typechecks.py b/shared-data/python/tests/deck/test_typechecks.py index cf41f603d1f..249bbf8c909 100644 --- a/shared-data/python/tests/deck/test_typechecks.py +++ b/shared-data/python/tests/deck/test_typechecks.py @@ -3,11 +3,12 @@ import pytest import typeguard -from opentrons_shared_data.deck import load as load_deck_definition +from opentrons_shared_data.deck import ( + list_names as list_deck_definition_names, + load as load_deck_definition, +) from opentrons_shared_data.deck.dev_types import DeckDefinitionV3, DeckDefinitionV4 -from . import list_deck_def_paths - pytestmark = pytest.mark.xfail( condition=sys.version_info >= (3, 10), @@ -15,13 +16,13 @@ ) -@pytest.mark.parametrize("defname", list_deck_def_paths(version=3)) +@pytest.mark.parametrize("defname", list_deck_definition_names(version=3)) def test_v3_defs(defname): defn = load_deck_definition(name=defname, version=3) typeguard.check_type("defn", defn, DeckDefinitionV3) -@pytest.mark.parametrize("defname", list_deck_def_paths(version=4)) +@pytest.mark.parametrize("defname", list_deck_definition_names(version=4)) def test_v4_defs(defname): defn = load_deck_definition(name=defname, version=4) typeguard.check_type("defn", defn, DeckDefinitionV4) diff --git a/shared-data/python/tests/pipette/test_load_data.py b/shared-data/python/tests/pipette/test_load_data.py index 84a6344ad1b..1b9e9775c16 100644 --- a/shared-data/python/tests/pipette/test_load_data.py +++ b/shared-data/python/tests/pipette/test_load_data.py @@ -85,6 +85,7 @@ def test_update_pipette_configuration( base_configurations = load_data.load_definition( model_name.pipette_type, model_name.pipette_channels, model_name.pipette_version ) + updated_configurations = load_data.update_pipette_configuration( base_configurations, v1_configuration_changes, liquid_class ) diff --git a/shared-data/python/tests/pipette/test_mutable_configurations.py b/shared-data/python/tests/pipette/test_mutable_configurations.py index e70520fb05f..3aabfd40434 100644 --- a/shared-data/python/tests/pipette/test_mutable_configurations.py +++ b/shared-data/python/tests/pipette/test_mutable_configurations.py @@ -241,7 +241,7 @@ def test_load_with_overrides( if serial_number == TEST_SERIAL_NUMBER: dict_loaded_configs = loaded_base_configurations.dict(by_alias=True) - dict_loaded_configs["pickUpTipConfigurations"]["speed"] = 5.0 + dict_loaded_configs["pickUpTipConfigurations"]["pressFit"]["speed"] = 5.0 updated_configurations_dict = updated_configurations.dict(by_alias=True) assert set(dict_loaded_configs.pop("quirks")) == set( updated_configurations_dict.pop("quirks") diff --git a/shared-data/python/tests/pipette/test_validate_schema.py b/shared-data/python/tests/pipette/test_validate_schema.py index 1ac8d111a16..df943dceace 100644 --- a/shared-data/python/tests/pipette/test_validate_schema.py +++ b/shared-data/python/tests/pipette/test_validate_schema.py @@ -18,6 +18,7 @@ def test_check_all_models_are_valid() -> None: "ninety_six_channel": "96", "eight_channel": "multi", } + assert os.listdir(paths_to_validate), "You have a path wrong" for channel_dir in os.listdir(paths_to_validate): for model_dir in os.listdir(paths_to_validate / channel_dir): for version_file in os.listdir(paths_to_validate / channel_dir / model_dir): diff --git a/shared-data/tsconfig.json b/shared-data/tsconfig.json index 1b1c50493db..bfeef7fb684 100644 --- a/shared-data/tsconfig.json +++ b/shared-data/tsconfig.json @@ -9,6 +9,7 @@ "include": [ "js", "protocol", + "deck", "command/types", "liquid/types", "commandAnnotation/types" diff --git a/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap b/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap index 496ad3aed3c..e8e545df2b3 100644 --- a/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap +++ b/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap @@ -496,6 +496,204 @@ Object { "H8": Object {}, "H9": Object {}, }, + "tiprack4AdapterId": Object {}, + "tiprack4Id": Object { + "A1": Object {}, + "A10": Object {}, + "A11": Object {}, + "A12": Object {}, + "A2": Object {}, + "A3": Object {}, + "A4": Object {}, + "A5": Object {}, + "A6": Object {}, + "A7": Object {}, + "A8": Object {}, + "A9": Object {}, + "B1": Object {}, + "B10": Object {}, + "B11": Object {}, + "B12": Object {}, + "B2": Object {}, + "B3": Object {}, + "B4": Object {}, + "B5": Object {}, + "B6": Object {}, + "B7": Object {}, + "B8": Object {}, + "B9": Object {}, + "C1": Object {}, + "C10": Object {}, + "C11": Object {}, + "C12": Object {}, + "C2": Object {}, + "C3": Object {}, + "C4": Object {}, + "C5": Object {}, + "C6": Object {}, + "C7": Object {}, + "C8": Object {}, + "C9": Object {}, + "D1": Object {}, + "D10": Object {}, + "D11": Object {}, + "D12": Object {}, + "D2": Object {}, + "D3": Object {}, + "D4": Object {}, + "D5": Object {}, + "D6": Object {}, + "D7": Object {}, + "D8": Object {}, + "D9": Object {}, + "E1": Object {}, + "E10": Object {}, + "E11": Object {}, + "E12": Object {}, + "E2": Object {}, + "E3": Object {}, + "E4": Object {}, + "E5": Object {}, + "E6": Object {}, + "E7": Object {}, + "E8": Object {}, + "E9": Object {}, + "F1": Object {}, + "F10": Object {}, + "F11": Object {}, + "F12": Object {}, + "F2": Object {}, + "F3": Object {}, + "F4": Object {}, + "F5": Object {}, + "F6": Object {}, + "F7": Object {}, + "F8": Object {}, + "F9": Object {}, + "G1": Object {}, + "G10": Object {}, + "G11": Object {}, + "G12": Object {}, + "G2": Object {}, + "G3": Object {}, + "G4": Object {}, + "G5": Object {}, + "G6": Object {}, + "G7": Object {}, + "G8": Object {}, + "G9": Object {}, + "H1": Object {}, + "H10": Object {}, + "H11": Object {}, + "H12": Object {}, + "H2": Object {}, + "H3": Object {}, + "H4": Object {}, + "H5": Object {}, + "H6": Object {}, + "H7": Object {}, + "H8": Object {}, + "H9": Object {}, + }, + "tiprack5AdapterId": Object {}, + "tiprack5Id": Object { + "A1": Object {}, + "A10": Object {}, + "A11": Object {}, + "A12": Object {}, + "A2": Object {}, + "A3": Object {}, + "A4": Object {}, + "A5": Object {}, + "A6": Object {}, + "A7": Object {}, + "A8": Object {}, + "A9": Object {}, + "B1": Object {}, + "B10": Object {}, + "B11": Object {}, + "B12": Object {}, + "B2": Object {}, + "B3": Object {}, + "B4": Object {}, + "B5": Object {}, + "B6": Object {}, + "B7": Object {}, + "B8": Object {}, + "B9": Object {}, + "C1": Object {}, + "C10": Object {}, + "C11": Object {}, + "C12": Object {}, + "C2": Object {}, + "C3": Object {}, + "C4": Object {}, + "C5": Object {}, + "C6": Object {}, + "C7": Object {}, + "C8": Object {}, + "C9": Object {}, + "D1": Object {}, + "D10": Object {}, + "D11": Object {}, + "D12": Object {}, + "D2": Object {}, + "D3": Object {}, + "D4": Object {}, + "D5": Object {}, + "D6": Object {}, + "D7": Object {}, + "D8": Object {}, + "D9": Object {}, + "E1": Object {}, + "E10": Object {}, + "E11": Object {}, + "E12": Object {}, + "E2": Object {}, + "E3": Object {}, + "E4": Object {}, + "E5": Object {}, + "E6": Object {}, + "E7": Object {}, + "E8": Object {}, + "E9": Object {}, + "F1": Object {}, + "F10": Object {}, + "F11": Object {}, + "F12": Object {}, + "F2": Object {}, + "F3": Object {}, + "F4": Object {}, + "F5": Object {}, + "F6": Object {}, + "F7": Object {}, + "F8": Object {}, + "F9": Object {}, + "G1": Object {}, + "G10": Object {}, + "G11": Object {}, + "G12": Object {}, + "G2": Object {}, + "G3": Object {}, + "G4": Object {}, + "G5": Object {}, + "G6": Object {}, + "G7": Object {}, + "G8": Object {}, + "G9": Object {}, + "H1": Object {}, + "H10": Object {}, + "H11": Object {}, + "H12": Object {}, + "H2": Object {}, + "H3": Object {}, + "H4": Object {}, + "H5": Object {}, + "H6": Object {}, + "H7": Object {}, + "H8": Object {}, + "H9": Object {}, + }, "troughId": Object { "A1": Object {}, "A10": Object {}, @@ -512,6 +710,104 @@ Object { }, }, "pipettes": Object { + "p100096Id": Object { + "0": Object {}, + "1": Object {}, + "10": Object {}, + "11": Object {}, + "12": Object {}, + "13": Object {}, + "14": Object {}, + "15": Object {}, + "16": Object {}, + "17": Object {}, + "18": Object {}, + "19": Object {}, + "2": Object {}, + "20": Object {}, + "21": Object {}, + "22": Object {}, + "23": Object {}, + "24": Object {}, + "25": Object {}, + "26": Object {}, + "27": Object {}, + "28": Object {}, + "29": Object {}, + "3": Object {}, + "30": Object {}, + "31": Object {}, + "32": Object {}, + "33": Object {}, + "34": Object {}, + "35": Object {}, + "36": Object {}, + "37": Object {}, + "38": Object {}, + "39": Object {}, + "4": Object {}, + "40": Object {}, + "41": Object {}, + "42": Object {}, + "43": Object {}, + "44": Object {}, + "45": Object {}, + "46": Object {}, + "47": Object {}, + "48": Object {}, + "49": Object {}, + "5": Object {}, + "50": Object {}, + "51": Object {}, + "52": Object {}, + "53": Object {}, + "54": Object {}, + "55": Object {}, + "56": Object {}, + "57": Object {}, + "58": Object {}, + "59": Object {}, + "6": Object {}, + "60": Object {}, + "61": Object {}, + "62": Object {}, + "63": Object {}, + "64": Object {}, + "65": Object {}, + "66": Object {}, + "67": Object {}, + "68": Object {}, + "69": Object {}, + "7": Object {}, + "70": Object {}, + "71": Object {}, + "72": Object {}, + "73": Object {}, + "74": Object {}, + "75": Object {}, + "76": Object {}, + "77": Object {}, + "78": Object {}, + "79": Object {}, + "8": Object {}, + "80": Object {}, + "81": Object {}, + "82": Object {}, + "83": Object {}, + "84": Object {}, + "85": Object {}, + "86": Object {}, + "87": Object {}, + "88": Object {}, + "89": Object {}, + "9": Object {}, + "90": Object {}, + "91": Object {}, + "92": Object {}, + "93": Object {}, + "94": Object {}, + "95": Object {}, + }, "p10MultiId": Object { "0": Object {}, "1": Object {}, @@ -6261,230 +6557,3755 @@ Object { "id": "tiprack3Id", "labwareDefURI": "fixture/fixture_tiprack_300_ul/1", }, - "troughId": Object { + "tiprack4AdapterId": Object { "def": Object { + "allowedRoles": Array [ + "adapter", + ], "brand": Object { - "brand": "USA Scientific", - "brandId": Array [ - "1061-8150", - ], + "brand": "Fixture", + "brandId": Array [], }, "cornerOffsetFromSlot": Object { - "x": 0, - "y": 0, + "x": -14.25, + "y": -3.5, "z": 0, }, "dimensions": Object { - "xDimension": 127.76, - "yDimension": 85.8, - "zDimension": 44.45, + "xDimension": 156.5, + "yDimension": 93, + "zDimension": 132, }, "groups": Array [ Object { - "metadata": Object { - "wellBottomShape": "v", - }, - "wells": Array [ - "A1", - "A2", - "A3", - "A4", - "A5", - "A6", - "A7", - "A8", - "A9", - "A10", - "A11", - "A12", - ], + "metadata": Object {}, + "wells": Array [], }, ], "metadata": Object { - "displayCategory": "reservoir", - "displayName": "12 Channel Trough", - "displayVolumeUnits": "mL", + "displayCategory": "adapter", + "displayName": "Fixture Flex 96 Tip Rack Adapter", + "displayVolumeUnits": "µL", + "tags": Array [], }, "namespace": "fixture", - "ordering": Array [ - Array [ - "A1", - ], - Array [ - "A2", - ], - Array [ - "A3", - ], - Array [ - "A4", - ], - Array [ - "A5", - ], - Array [ - "A6", - ], + "ordering": Array [], + "parameters": Object { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "fixture_flex_96_tiprack_adapter", + "quirks": Array [], + }, + "schemaVersion": 2, + "version": 1, + "wells": Object {}, + }, + "id": "tiprack4AdapterId", + "labwareDefURI": "fixture/fixture_flex_96_tiprack_adapter/1", + }, + "tiprack4Id": Object { + "def": Object { + "brand": Object { + "brand": "Fixture", + "brandId": Array [], + }, + "cornerOffsetFromSlot": Object { + "x": 0, + "y": 0, + "z": 0, + }, + "dimensions": Object { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99, + }, + "gripForce": 16, + "gripHeightFromLabwareBottom": 23.9, + "groups": Array [ + Object { + "metadata": Object {}, + "wells": Array [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12", + ], + }, + ], + "metadata": Object { + "displayCategory": "tipRack", + "displayName": "Fixture Flex Tiprack 1000 uL", + "displayVolumeUnits": "µL", + "tags": Array [], + }, + "namespace": "fixture", + "ordering": Array [ + Array [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + ], + Array [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + ], + Array [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + ], + Array [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + ], + Array [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + ], + Array [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + ], Array [ "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", ], Array [ "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", ], Array [ "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", ], Array [ "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", ], Array [ "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", ], Array [ "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12", ], ], "parameters": Object { - "format": "trough", + "format": "96Standard", "isMagneticModuleCompatible": false, - "isTiprack": false, - "loadName": "fixture_12_trough", - "quirks": Array [ - "centerMultichannelOnWells", - "touchTipDisabled", - ], + "isTiprack": true, + "loadName": "fixture_flex_96_tiprack_1000ul", + "quirks": Array [], + "tipLength": 95.6, + "tipOverlap": 10.5, }, "schemaVersion": 2, + "stackingOffsetWithLabware": Object { + "opentrons_flex_96_tiprack_adapter": Object { + "x": 0, + "y": 0, + "z": 121, + }, + }, "version": 1, "wells": Object { "A1": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 13.94, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 74.38, + "z": 1.5, }, "A10": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 95.75, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 74.38, + "z": 1.5, }, "A11": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 104.84, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 74.38, + "z": 1.5, }, "A12": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 113.93, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 74.38, + "z": 1.5, }, "A2": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 23.03, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 74.38, + "z": 1.5, }, "A3": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 32.12, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 74.38, + "z": 1.5, }, "A4": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 41.21, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 74.38, + "z": 1.5, }, "A5": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 50.3, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 74.38, + "z": 1.5, }, "A6": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 59.39, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 74.38, + "z": 1.5, + }, + "A7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 74.38, + "z": 1.5, + }, + "A8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 74.38, + "z": 1.5, + }, + "A9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 74.38, + "z": 1.5, + }, + "B1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 65.38, + "z": 1.5, + }, + "B10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 65.38, + "z": 1.5, + }, + "B11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 65.38, + "z": 1.5, + }, + "B12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 65.38, + "z": 1.5, + }, + "B2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 65.38, + "z": 1.5, + }, + "B3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 65.38, + "z": 1.5, + }, + "B4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 65.38, + "z": 1.5, + }, + "B5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 65.38, + "z": 1.5, + }, + "B6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 65.38, + "z": 1.5, + }, + "B7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 65.38, + "z": 1.5, + }, + "B8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 65.38, + "z": 1.5, + }, + "B9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 65.38, + "z": 1.5, + }, + "C1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 56.38, + "z": 1.5, + }, + "C10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 56.38, + "z": 1.5, + }, + "C11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 56.38, + "z": 1.5, + }, + "C12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 56.38, + "z": 1.5, + }, + "C2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 56.38, + "z": 1.5, + }, + "C3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 56.38, + "z": 1.5, + }, + "C4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 56.38, + "z": 1.5, + }, + "C5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 56.38, + "z": 1.5, + }, + "C6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 56.38, + "z": 1.5, + }, + "C7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 56.38, + "z": 1.5, + }, + "C8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 56.38, + "z": 1.5, + }, + "C9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 56.38, + "z": 1.5, + }, + "D1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 47.38, + "z": 1.5, + }, + "D10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 47.38, + "z": 1.5, + }, + "D11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 47.38, + "z": 1.5, + }, + "D12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 47.38, + "z": 1.5, + }, + "D2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 47.38, + "z": 1.5, + }, + "D3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 47.38, + "z": 1.5, + }, + "D4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 47.38, + "z": 1.5, + }, + "D5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 47.38, + "z": 1.5, + }, + "D6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 47.38, + "z": 1.5, + }, + "D7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 47.38, + "z": 1.5, + }, + "D8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 47.38, + "z": 1.5, + }, + "D9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 47.38, + "z": 1.5, + }, + "E1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 38.38, + "z": 1.5, + }, + "E10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 38.38, + "z": 1.5, + }, + "E11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 38.38, + "z": 1.5, + }, + "E12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 38.38, + "z": 1.5, + }, + "E2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 38.38, + "z": 1.5, + }, + "E3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 38.38, + "z": 1.5, + }, + "E4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 38.38, + "z": 1.5, + }, + "E5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 38.38, + "z": 1.5, + }, + "E6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 38.38, + "z": 1.5, + }, + "E7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 38.38, + "z": 1.5, + }, + "E8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 38.38, + "z": 1.5, + }, + "E9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 38.38, + "z": 1.5, + }, + "F1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 29.38, + "z": 1.5, + }, + "F10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 29.38, + "z": 1.5, + }, + "F11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 29.38, + "z": 1.5, + }, + "F12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 29.38, + "z": 1.5, + }, + "F2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 29.38, + "z": 1.5, + }, + "F3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 29.38, + "z": 1.5, + }, + "F4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 29.38, + "z": 1.5, + }, + "F5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 29.38, + "z": 1.5, + }, + "F6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 29.38, + "z": 1.5, + }, + "F7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 29.38, + "z": 1.5, + }, + "F8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 29.38, + "z": 1.5, + }, + "F9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 29.38, + "z": 1.5, + }, + "G1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 20.38, + "z": 1.5, + }, + "G10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 20.38, + "z": 1.5, + }, + "G11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 20.38, + "z": 1.5, + }, + "G12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 20.38, + "z": 1.5, + }, + "G2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 20.38, + "z": 1.5, + }, + "G3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 20.38, + "z": 1.5, + }, + "G4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 20.38, + "z": 1.5, + }, + "G5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 20.38, + "z": 1.5, + }, + "G6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 20.38, + "z": 1.5, + }, + "G7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 20.38, + "z": 1.5, + }, + "G8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 20.38, + "z": 1.5, + }, + "G9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 20.38, + "z": 1.5, + }, + "H1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 11.38, + "z": 1.5, + }, + "H10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 11.38, + "z": 1.5, + }, + "H11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 11.38, + "z": 1.5, + }, + "H12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 11.38, + "z": 1.5, + }, + "H2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 11.38, + "z": 1.5, + }, + "H3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 11.38, + "z": 1.5, + }, + "H4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 11.38, + "z": 1.5, + }, + "H5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 11.38, + "z": 1.5, + }, + "H6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 11.38, + "z": 1.5, + }, + "H7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 11.38, + "z": 1.5, + }, + "H8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 11.38, + "z": 1.5, + }, + "H9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 11.38, + "z": 1.5, + }, + }, + }, + "id": "tiprack4Id", + "labwareDefURI": "fixture/fixture_flex_96_tiprack_1000ul/1", + }, + "tiprack5AdapterId": Object { + "def": Object { + "allowedRoles": Array [ + "adapter", + ], + "brand": Object { + "brand": "Fixture", + "brandId": Array [], + }, + "cornerOffsetFromSlot": Object { + "x": -14.25, + "y": -3.5, + "z": 0, + }, + "dimensions": Object { + "xDimension": 156.5, + "yDimension": 93, + "zDimension": 132, + }, + "groups": Array [ + Object { + "metadata": Object {}, + "wells": Array [], + }, + ], + "metadata": Object { + "displayCategory": "adapter", + "displayName": "Fixture Flex 96 Tip Rack Adapter", + "displayVolumeUnits": "µL", + "tags": Array [], + }, + "namespace": "fixture", + "ordering": Array [], + "parameters": Object { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "fixture_flex_96_tiprack_adapter", + "quirks": Array [], + }, + "schemaVersion": 2, + "version": 1, + "wells": Object {}, + }, + "id": "tiprack5AdapterId", + "labwareDefURI": "fixture/fixture_flex_96_tiprack_adapter/1", + }, + "tiprack5Id": Object { + "def": Object { + "brand": Object { + "brand": "Fixture", + "brandId": Array [], + }, + "cornerOffsetFromSlot": Object { + "x": 0, + "y": 0, + "z": 0, + }, + "dimensions": Object { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99, + }, + "gripForce": 16, + "gripHeightFromLabwareBottom": 23.9, + "groups": Array [ + Object { + "metadata": Object {}, + "wells": Array [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12", + ], + }, + ], + "metadata": Object { + "displayCategory": "tipRack", + "displayName": "Fixture Flex Tiprack 1000 uL", + "displayVolumeUnits": "µL", + "tags": Array [], + }, + "namespace": "fixture", + "ordering": Array [ + Array [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + ], + Array [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + ], + Array [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + ], + Array [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + ], + Array [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + ], + Array [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + ], + Array [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + ], + Array [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + ], + Array [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + ], + Array [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + ], + Array [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + ], + Array [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12", + ], + ], + "parameters": Object { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": true, + "loadName": "fixture_flex_96_tiprack_1000ul", + "quirks": Array [], + "tipLength": 95.6, + "tipOverlap": 10.5, + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": Object { + "opentrons_flex_96_tiprack_adapter": Object { + "x": 0, + "y": 0, + "z": 121, + }, + }, + "version": 1, + "wells": Object { + "A1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 74.38, + "z": 1.5, + }, + "A10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 74.38, + "z": 1.5, + }, + "A11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 74.38, + "z": 1.5, + }, + "A12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 74.38, + "z": 1.5, + }, + "A2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 74.38, + "z": 1.5, + }, + "A3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 74.38, + "z": 1.5, + }, + "A4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 74.38, + "z": 1.5, + }, + "A5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 74.38, + "z": 1.5, + }, + "A6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 74.38, + "z": 1.5, + }, + "A7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 74.38, + "z": 1.5, + }, + "A8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 74.38, + "z": 1.5, + }, + "A9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 74.38, + "z": 1.5, + }, + "B1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 65.38, + "z": 1.5, + }, + "B10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 65.38, + "z": 1.5, + }, + "B11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 65.38, + "z": 1.5, + }, + "B12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 65.38, + "z": 1.5, + }, + "B2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 65.38, + "z": 1.5, + }, + "B3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 65.38, + "z": 1.5, + }, + "B4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 65.38, + "z": 1.5, + }, + "B5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 65.38, + "z": 1.5, + }, + "B6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 65.38, + "z": 1.5, + }, + "B7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 65.38, + "z": 1.5, + }, + "B8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 65.38, + "z": 1.5, + }, + "B9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 65.38, + "z": 1.5, + }, + "C1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 56.38, + "z": 1.5, + }, + "C10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 56.38, + "z": 1.5, + }, + "C11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 56.38, + "z": 1.5, + }, + "C12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 56.38, + "z": 1.5, + }, + "C2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 56.38, + "z": 1.5, + }, + "C3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 56.38, + "z": 1.5, + }, + "C4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 56.38, + "z": 1.5, + }, + "C5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 56.38, + "z": 1.5, + }, + "C6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 56.38, + "z": 1.5, + }, + "C7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 56.38, + "z": 1.5, + }, + "C8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 56.38, + "z": 1.5, + }, + "C9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 56.38, + "z": 1.5, + }, + "D1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 47.38, + "z": 1.5, + }, + "D10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 47.38, + "z": 1.5, + }, + "D11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 47.38, + "z": 1.5, + }, + "D12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 47.38, + "z": 1.5, + }, + "D2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 47.38, + "z": 1.5, + }, + "D3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 47.38, + "z": 1.5, + }, + "D4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 47.38, + "z": 1.5, + }, + "D5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 47.38, + "z": 1.5, + }, + "D6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 47.38, + "z": 1.5, + }, + "D7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 47.38, + "z": 1.5, + }, + "D8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 47.38, + "z": 1.5, + }, + "D9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 47.38, + "z": 1.5, + }, + "E1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 38.38, + "z": 1.5, + }, + "E10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 38.38, + "z": 1.5, + }, + "E11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 38.38, + "z": 1.5, + }, + "E12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 38.38, + "z": 1.5, + }, + "E2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 38.38, + "z": 1.5, + }, + "E3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 38.38, + "z": 1.5, + }, + "E4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 38.38, + "z": 1.5, + }, + "E5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 38.38, + "z": 1.5, + }, + "E6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 38.38, + "z": 1.5, + }, + "E7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 38.38, + "z": 1.5, + }, + "E8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 38.38, + "z": 1.5, + }, + "E9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 38.38, + "z": 1.5, + }, + "F1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 29.38, + "z": 1.5, + }, + "F10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 29.38, + "z": 1.5, + }, + "F11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 29.38, + "z": 1.5, + }, + "F12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 29.38, + "z": 1.5, + }, + "F2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 29.38, + "z": 1.5, + }, + "F3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 29.38, + "z": 1.5, + }, + "F4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 29.38, + "z": 1.5, + }, + "F5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 29.38, + "z": 1.5, + }, + "F6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 29.38, + "z": 1.5, + }, + "F7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 29.38, + "z": 1.5, + }, + "F8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 29.38, + "z": 1.5, + }, + "F9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 29.38, + "z": 1.5, + }, + "G1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 20.38, + "z": 1.5, + }, + "G10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 20.38, + "z": 1.5, + }, + "G11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 20.38, + "z": 1.5, + }, + "G12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 20.38, + "z": 1.5, + }, + "G2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 20.38, + "z": 1.5, + }, + "G3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 20.38, + "z": 1.5, + }, + "G4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 20.38, + "z": 1.5, + }, + "G5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 20.38, + "z": 1.5, + }, + "G6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 20.38, + "z": 1.5, + }, + "G7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 20.38, + "z": 1.5, + }, + "G8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 20.38, + "z": 1.5, + }, + "G9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 20.38, + "z": 1.5, + }, + "H1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 11.38, + "z": 1.5, + }, + "H10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 11.38, + "z": 1.5, + }, + "H11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 11.38, + "z": 1.5, + }, + "H12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 11.38, + "z": 1.5, + }, + "H2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 11.38, + "z": 1.5, + }, + "H3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 11.38, + "z": 1.5, + }, + "H4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 11.38, + "z": 1.5, + }, + "H5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 11.38, + "z": 1.5, + }, + "H6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 11.38, + "z": 1.5, + }, + "H7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 11.38, + "z": 1.5, + }, + "H8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 11.38, + "z": 1.5, + }, + "H9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 11.38, + "z": 1.5, + }, + }, + }, + "id": "tiprack5Id", + "labwareDefURI": "fixture/fixture_flex_96_tiprack_1000ul/1", + }, + "troughId": Object { + "def": Object { + "brand": Object { + "brand": "USA Scientific", + "brandId": Array [ + "1061-8150", + ], + }, + "cornerOffsetFromSlot": Object { + "x": 0, + "y": 0, + "z": 0, + }, + "dimensions": Object { + "xDimension": 127.76, + "yDimension": 85.8, + "zDimension": 44.45, + }, + "groups": Array [ + Object { + "metadata": Object { + "wellBottomShape": "v", + }, + "wells": Array [ + "A1", + "A2", + "A3", + "A4", + "A5", + "A6", + "A7", + "A8", + "A9", + "A10", + "A11", + "A12", + ], + }, + ], + "metadata": Object { + "displayCategory": "reservoir", + "displayName": "12 Channel Trough", + "displayVolumeUnits": "mL", + }, + "namespace": "fixture", + "ordering": Array [ + Array [ + "A1", + ], + Array [ + "A2", + ], + Array [ + "A3", + ], + Array [ + "A4", + ], + Array [ + "A5", + ], + Array [ + "A6", + ], + Array [ + "A7", + ], + Array [ + "A8", + ], + Array [ + "A9", + ], + Array [ + "A10", + ], + Array [ + "A11", + ], + Array [ + "A12", + ], + ], + "parameters": Object { + "format": "trough", + "isMagneticModuleCompatible": false, + "isTiprack": false, + "loadName": "fixture_12_trough", + "quirks": Array [ + "centerMultichannelOnWells", + "touchTipDisabled", + ], + }, + "schemaVersion": 2, + "version": 1, + "wells": Object { + "A1": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 13.94, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A10": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 95.75, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A11": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 104.84, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A12": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 113.93, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A2": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 23.03, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A3": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 32.12, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A4": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 41.21, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A5": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 50.3, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A6": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 59.39, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A7": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 68.48, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A8": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 77.57, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + "A9": Object { + "depth": 42.16, + "shape": "rectangular", + "totalLiquidVolume": 22000, + "x": 86.66, + "xDimension": 8.33, + "y": 42.9, + "yDimension": 71.88, + "z": 2.29, + }, + }, + }, + "id": "troughId", + "labwareDefURI": "fixture/fixture_12_trough/1", + }, + }, + "moduleEntities": Object {}, + "pipetteEntities": Object { + "p100096Id": Object { + "id": "p100096Id", + "name": "p1000_96", + "spec": Object { + "channels": 96, + "defaultAspirateFlowRate": Object { + "max": 812, + "min": 3, + "value": 7.85, + }, + "defaultDispenseFlowRate": Object { + "max": 812, + "min": 3, + "value": 7.85, + }, + "displayName": "Flex 96-Channel 1000 μL", + "maxVolume": 1000, + "minVolume": 5, + }, + "tiprackDefURI": "fixture/fixture_flex_96_tiprack_1000ul/1", + "tiprackLabwareDef": Object { + "brand": Object { + "brand": "Fixture", + "brandId": Array [], + }, + "cornerOffsetFromSlot": Object { + "x": 0, + "y": 0, + "z": 0, + }, + "dimensions": Object { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99, + }, + "gripForce": 16, + "gripHeightFromLabwareBottom": 23.9, + "groups": Array [ + Object { + "metadata": Object {}, + "wells": Array [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12", + ], + }, + ], + "metadata": Object { + "displayCategory": "tipRack", + "displayName": "Fixture Flex Tiprack 1000 uL", + "displayVolumeUnits": "µL", + "tags": Array [], + }, + "namespace": "fixture", + "ordering": Array [ + Array [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + ], + Array [ + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + ], + Array [ + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + ], + Array [ + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + ], + Array [ + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + ], + Array [ + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + ], + Array [ + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + ], + Array [ + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + ], + Array [ + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + ], + Array [ + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + ], + Array [ + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + ], + Array [ + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12", + ], + ], + "parameters": Object { + "format": "96Standard", + "isMagneticModuleCompatible": false, + "isTiprack": true, + "loadName": "fixture_flex_96_tiprack_1000ul", + "quirks": Array [], + "tipLength": 95.6, + "tipOverlap": 10.5, + }, + "schemaVersion": 2, + "stackingOffsetWithLabware": Object { + "opentrons_flex_96_tiprack_adapter": Object { + "x": 0, + "y": 0, + "z": 121, + }, + }, + "version": 1, + "wells": Object { + "A1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 74.38, + "z": 1.5, + }, + "A10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 74.38, + "z": 1.5, + }, + "A11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 74.38, + "z": 1.5, + }, + "A12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 74.38, + "z": 1.5, + }, + "A2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 74.38, + "z": 1.5, + }, + "A3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 74.38, + "z": 1.5, + }, + "A4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 74.38, + "z": 1.5, + }, + "A5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 74.38, + "z": 1.5, + }, + "A6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 74.38, + "z": 1.5, + }, + "A7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 74.38, + "z": 1.5, + }, + "A8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 74.38, + "z": 1.5, + }, + "A9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 74.38, + "z": 1.5, + }, + "B1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 65.38, + "z": 1.5, + }, + "B10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 65.38, + "z": 1.5, + }, + "B11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 65.38, + "z": 1.5, + }, + "B12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 65.38, + "z": 1.5, + }, + "B2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 65.38, + "z": 1.5, + }, + "B3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 65.38, + "z": 1.5, + }, + "B4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 65.38, + "z": 1.5, + }, + "B5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 65.38, + "z": 1.5, + }, + "B6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 65.38, + "z": 1.5, + }, + "B7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 65.38, + "z": 1.5, + }, + "B8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 65.38, + "z": 1.5, + }, + "B9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 65.38, + "z": 1.5, + }, + "C1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 56.38, + "z": 1.5, + }, + "C10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 56.38, + "z": 1.5, + }, + "C11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 56.38, + "z": 1.5, + }, + "C12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 56.38, + "z": 1.5, + }, + "C2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 56.38, + "z": 1.5, + }, + "C3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 56.38, + "z": 1.5, + }, + "C4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 56.38, + "z": 1.5, + }, + "C5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 56.38, + "z": 1.5, + }, + "C6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 56.38, + "z": 1.5, + }, + "C7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 56.38, + "z": 1.5, + }, + "C8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 56.38, + "z": 1.5, + }, + "C9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 56.38, + "z": 1.5, + }, + "D1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 47.38, + "z": 1.5, + }, + "D10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 47.38, + "z": 1.5, + }, + "D11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 47.38, + "z": 1.5, + }, + "D12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 47.38, + "z": 1.5, + }, + "D2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 47.38, + "z": 1.5, + }, + "D3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 47.38, + "z": 1.5, + }, + "D4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 47.38, + "z": 1.5, + }, + "D5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 47.38, + "z": 1.5, + }, + "D6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 47.38, + "z": 1.5, + }, + "D7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 47.38, + "z": 1.5, + }, + "D8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 47.38, + "z": 1.5, + }, + "D9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 47.38, + "z": 1.5, + }, + "E1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 38.38, + "z": 1.5, + }, + "E10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 38.38, + "z": 1.5, + }, + "E11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 38.38, + "z": 1.5, + }, + "E12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 38.38, + "z": 1.5, + }, + "E2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 38.38, + "z": 1.5, + }, + "E3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 38.38, + "z": 1.5, + }, + "E4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 38.38, + "z": 1.5, + }, + "E5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 38.38, + "z": 1.5, + }, + "E6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 38.38, + "z": 1.5, + }, + "E7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 38.38, + "z": 1.5, + }, + "E8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 38.38, + "z": 1.5, + }, + "E9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 38.38, + "z": 1.5, + }, + "F1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 29.38, + "z": 1.5, + }, + "F10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 29.38, + "z": 1.5, + }, + "F11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 29.38, + "z": 1.5, + }, + "F12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 29.38, + "z": 1.5, + }, + "F2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 29.38, + "z": 1.5, + }, + "F3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 29.38, + "z": 1.5, + }, + "F4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 29.38, + "z": 1.5, + }, + "F5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 29.38, + "z": 1.5, + }, + "F6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 29.38, + "z": 1.5, + }, + "F7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 29.38, + "z": 1.5, + }, + "F8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 29.38, + "z": 1.5, + }, + "F9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 29.38, + "z": 1.5, + }, + "G1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 20.38, + "z": 1.5, + }, + "G10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 20.38, + "z": 1.5, + }, + "G11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 20.38, + "z": 1.5, + }, + "G12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 20.38, + "z": 1.5, + }, + "G2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 20.38, + "z": 1.5, + }, + "G3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 20.38, + "z": 1.5, + }, + "G4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 20.38, + "z": 1.5, + }, + "G5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 20.38, + "z": 1.5, + }, + "G6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 20.38, + "z": 1.5, + }, + "G7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 20.38, + "z": 1.5, + }, + "G8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 20.38, + "z": 1.5, + }, + "G9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 20.38, + "z": 1.5, + }, + "H1": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 11.38, + "z": 1.5, + }, + "H10": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 11.38, + "z": 1.5, + }, + "H11": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 11.38, + "z": 1.5, + }, + "H12": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 11.38, + "z": 1.5, + }, + "H2": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 11.38, + "z": 1.5, + }, + "H3": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 11.38, + "z": 1.5, + }, + "H4": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 11.38, + "z": 1.5, }, - "A7": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 68.48, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "H5": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 11.38, + "z": 1.5, }, - "A8": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 77.57, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "H6": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 11.38, + "z": 1.5, }, - "A9": Object { - "depth": 42.16, - "shape": "rectangular", - "totalLiquidVolume": 22000, - "x": 86.66, - "xDimension": 8.33, - "y": 42.9, - "yDimension": 71.88, - "z": 2.29, + "H7": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 11.38, + "z": 1.5, + }, + "H8": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 11.38, + "z": 1.5, + }, + "H9": Object { + "depth": 97.5, + "diameter": 5.47, + "shape": "circular", + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 11.38, + "z": 1.5, }, }, }, - "id": "troughId", - "labwareDefURI": "fixture/fixture_12_trough/1", }, - }, - "moduleEntities": Object {}, - "pipetteEntities": Object { "p10MultiId": Object { "id": "p10MultiId", "name": "p10_multi", @@ -11088,6 +14909,18 @@ Object { "tiprack2Id": Object { "slot": "2", }, + "tiprack4AdapterId": Object { + "slot": "7", + }, + "tiprack4Id": Object { + "slot": "tiprack4AdapterId", + }, + "tiprack5AdapterId": Object { + "slot": "8", + }, + "tiprack5Id": Object { + "slot": "tiprack5AdapterId", + }, }, "liquidState": Object { "labware": Object { @@ -11584,6 +15417,204 @@ Object { "H8": Object {}, "H9": Object {}, }, + "tiprack4AdapterId": Object {}, + "tiprack4Id": Object { + "A1": Object {}, + "A10": Object {}, + "A11": Object {}, + "A12": Object {}, + "A2": Object {}, + "A3": Object {}, + "A4": Object {}, + "A5": Object {}, + "A6": Object {}, + "A7": Object {}, + "A8": Object {}, + "A9": Object {}, + "B1": Object {}, + "B10": Object {}, + "B11": Object {}, + "B12": Object {}, + "B2": Object {}, + "B3": Object {}, + "B4": Object {}, + "B5": Object {}, + "B6": Object {}, + "B7": Object {}, + "B8": Object {}, + "B9": Object {}, + "C1": Object {}, + "C10": Object {}, + "C11": Object {}, + "C12": Object {}, + "C2": Object {}, + "C3": Object {}, + "C4": Object {}, + "C5": Object {}, + "C6": Object {}, + "C7": Object {}, + "C8": Object {}, + "C9": Object {}, + "D1": Object {}, + "D10": Object {}, + "D11": Object {}, + "D12": Object {}, + "D2": Object {}, + "D3": Object {}, + "D4": Object {}, + "D5": Object {}, + "D6": Object {}, + "D7": Object {}, + "D8": Object {}, + "D9": Object {}, + "E1": Object {}, + "E10": Object {}, + "E11": Object {}, + "E12": Object {}, + "E2": Object {}, + "E3": Object {}, + "E4": Object {}, + "E5": Object {}, + "E6": Object {}, + "E7": Object {}, + "E8": Object {}, + "E9": Object {}, + "F1": Object {}, + "F10": Object {}, + "F11": Object {}, + "F12": Object {}, + "F2": Object {}, + "F3": Object {}, + "F4": Object {}, + "F5": Object {}, + "F6": Object {}, + "F7": Object {}, + "F8": Object {}, + "F9": Object {}, + "G1": Object {}, + "G10": Object {}, + "G11": Object {}, + "G12": Object {}, + "G2": Object {}, + "G3": Object {}, + "G4": Object {}, + "G5": Object {}, + "G6": Object {}, + "G7": Object {}, + "G8": Object {}, + "G9": Object {}, + "H1": Object {}, + "H10": Object {}, + "H11": Object {}, + "H12": Object {}, + "H2": Object {}, + "H3": Object {}, + "H4": Object {}, + "H5": Object {}, + "H6": Object {}, + "H7": Object {}, + "H8": Object {}, + "H9": Object {}, + }, + "tiprack5AdapterId": Object {}, + "tiprack5Id": Object { + "A1": Object {}, + "A10": Object {}, + "A11": Object {}, + "A12": Object {}, + "A2": Object {}, + "A3": Object {}, + "A4": Object {}, + "A5": Object {}, + "A6": Object {}, + "A7": Object {}, + "A8": Object {}, + "A9": Object {}, + "B1": Object {}, + "B10": Object {}, + "B11": Object {}, + "B12": Object {}, + "B2": Object {}, + "B3": Object {}, + "B4": Object {}, + "B5": Object {}, + "B6": Object {}, + "B7": Object {}, + "B8": Object {}, + "B9": Object {}, + "C1": Object {}, + "C10": Object {}, + "C11": Object {}, + "C12": Object {}, + "C2": Object {}, + "C3": Object {}, + "C4": Object {}, + "C5": Object {}, + "C6": Object {}, + "C7": Object {}, + "C8": Object {}, + "C9": Object {}, + "D1": Object {}, + "D10": Object {}, + "D11": Object {}, + "D12": Object {}, + "D2": Object {}, + "D3": Object {}, + "D4": Object {}, + "D5": Object {}, + "D6": Object {}, + "D7": Object {}, + "D8": Object {}, + "D9": Object {}, + "E1": Object {}, + "E10": Object {}, + "E11": Object {}, + "E12": Object {}, + "E2": Object {}, + "E3": Object {}, + "E4": Object {}, + "E5": Object {}, + "E6": Object {}, + "E7": Object {}, + "E8": Object {}, + "E9": Object {}, + "F1": Object {}, + "F10": Object {}, + "F11": Object {}, + "F12": Object {}, + "F2": Object {}, + "F3": Object {}, + "F4": Object {}, + "F5": Object {}, + "F6": Object {}, + "F7": Object {}, + "F8": Object {}, + "F9": Object {}, + "G1": Object {}, + "G10": Object {}, + "G11": Object {}, + "G12": Object {}, + "G2": Object {}, + "G3": Object {}, + "G4": Object {}, + "G5": Object {}, + "G6": Object {}, + "G7": Object {}, + "G8": Object {}, + "G9": Object {}, + "H1": Object {}, + "H10": Object {}, + "H11": Object {}, + "H12": Object {}, + "H2": Object {}, + "H3": Object {}, + "H4": Object {}, + "H5": Object {}, + "H6": Object {}, + "H7": Object {}, + "H8": Object {}, + "H9": Object {}, + }, "troughId": Object { "A1": Object {}, "A10": Object {}, @@ -11600,6 +15631,104 @@ Object { }, }, "pipettes": Object { + "p100096Id": Object { + "0": Object {}, + "1": Object {}, + "10": Object {}, + "11": Object {}, + "12": Object {}, + "13": Object {}, + "14": Object {}, + "15": Object {}, + "16": Object {}, + "17": Object {}, + "18": Object {}, + "19": Object {}, + "2": Object {}, + "20": Object {}, + "21": Object {}, + "22": Object {}, + "23": Object {}, + "24": Object {}, + "25": Object {}, + "26": Object {}, + "27": Object {}, + "28": Object {}, + "29": Object {}, + "3": Object {}, + "30": Object {}, + "31": Object {}, + "32": Object {}, + "33": Object {}, + "34": Object {}, + "35": Object {}, + "36": Object {}, + "37": Object {}, + "38": Object {}, + "39": Object {}, + "4": Object {}, + "40": Object {}, + "41": Object {}, + "42": Object {}, + "43": Object {}, + "44": Object {}, + "45": Object {}, + "46": Object {}, + "47": Object {}, + "48": Object {}, + "49": Object {}, + "5": Object {}, + "50": Object {}, + "51": Object {}, + "52": Object {}, + "53": Object {}, + "54": Object {}, + "55": Object {}, + "56": Object {}, + "57": Object {}, + "58": Object {}, + "59": Object {}, + "6": Object {}, + "60": Object {}, + "61": Object {}, + "62": Object {}, + "63": Object {}, + "64": Object {}, + "65": Object {}, + "66": Object {}, + "67": Object {}, + "68": Object {}, + "69": Object {}, + "7": Object {}, + "70": Object {}, + "71": Object {}, + "72": Object {}, + "73": Object {}, + "74": Object {}, + "75": Object {}, + "76": Object {}, + "77": Object {}, + "78": Object {}, + "79": Object {}, + "8": Object {}, + "80": Object {}, + "81": Object {}, + "82": Object {}, + "83": Object {}, + "84": Object {}, + "85": Object {}, + "86": Object {}, + "87": Object {}, + "88": Object {}, + "89": Object {}, + "9": Object {}, + "90": Object {}, + "91": Object {}, + "92": Object {}, + "93": Object {}, + "94": Object {}, + "95": Object {}, + }, "p10MultiId": Object { "0": Object {}, "1": Object {}, diff --git a/step-generation/src/__tests__/aspirate.test.ts b/step-generation/src/__tests__/aspirate.test.ts index 4c6b3ab8911..ab9b7869327 100644 --- a/step-generation/src/__tests__/aspirate.test.ts +++ b/step-generation/src/__tests__/aspirate.test.ts @@ -198,6 +198,29 @@ describe('aspirate', () => { type: 'LABWARE_DOES_NOT_EXIST', }) }) + it('should return an error when aspirating from the 4th column', () => { + robotStateWithTip = { + ...robotStateWithTip, + labware: { + [SOURCE_LABWARE]: { slot: 'A4' }, + }, + } + const result = aspirate( + { + ...flowRateAndOffsets, + pipette: DEFAULT_PIPETTE, + volume: 50, + labware: SOURCE_LABWARE, + well: 'A1', + } as AspDispAirgapParams, + invariantContext, + robotStateWithTip + ) + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ + type: 'PIPETTING_INTO_COLUMN_4', + }) + }) it('should return an error when aspirating from labware off deck', () => { initialRobotState = getInitialRobotStateWithOffDeckLabwareStandard( invariantContext diff --git a/step-generation/src/__tests__/consolidate.test.ts b/step-generation/src/__tests__/consolidate.test.ts index 1b1e10dd555..ad0a1d0adfb 100644 --- a/step-generation/src/__tests__/consolidate.test.ts +++ b/step-generation/src/__tests__/consolidate.test.ts @@ -22,6 +22,8 @@ import { pickUpTipHelper, SOURCE_LABWARE, AIR_GAP_META, + moveToAddressableAreaHelper, + dropTipInPlaceHelper, } from '../fixtures' import { DEST_WELL_BLOWOUT_DESTINATION } from '../utils' import type { AspDispAirgapParams, CreateCommand } from '@opentrons/shared-data' @@ -120,6 +122,43 @@ describe('consolidate single-channel', () => { ]) }) + it('Minimal single-channel: A1 A2 to B1, 50uL with p300, drop in waste chute', () => { + const data = { + ...mixinArgs, + sourceWells: ['A1', 'A2'], + volume: 50, + changeTip: 'once', + dropTipLocation: 'wasteChuteId', + dispenseAirGapVolume: 5, + } as ConsolidateArgs + + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: { + wasteChuteId: { + name: 'wasteChute', + id: 'wasteChuteId', + location: 'cutoutD3', + }, + }, + } + + const result = consolidate(data, invariantContext, initialRobotState) + const res = getSuccessResult(result) + + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), + aspirateHelper('A1', 50), + aspirateHelper('A2', 50), + dispenseHelper('B1', 100), + airGapHelper('B1', 5, { labwareId: 'destPlateId' }), + moveToAddressableAreaHelper({ + addressableAreaName: '1and8ChannelWasteChute', + }), + dropTipInPlaceHelper(), + ]) + }) + it('Single-channel with exceeding pipette max: A1 A2 A3 A4 to B1, 150uL with p300', () => { // TODO Ian 2018-05-03 is this a duplicate of exceeding max with changeTip="once"??? const data = { diff --git a/step-generation/src/__tests__/dispense.test.ts b/step-generation/src/__tests__/dispense.test.ts index b1399c54c4a..d66fae15b5e 100644 --- a/step-generation/src/__tests__/dispense.test.ts +++ b/step-generation/src/__tests__/dispense.test.ts @@ -141,6 +141,19 @@ describe('dispense', () => { type: 'LABWARE_DOES_NOT_EXIST', }) }) + it('should return an error when dispensing from the 4th column', () => { + robotStateWithTip = { + ...robotStateWithTip, + labware: { + [SOURCE_LABWARE]: { slot: 'A4' }, + }, + } + const result = dispense(params, invariantContext, robotStateWithTip) + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ + type: 'PIPETTING_INTO_COLUMN_4', + }) + }) it('should return an error when dispensing into thermocycler with pipette collision', () => { mockThermocyclerPipetteCollision.mockImplementationOnce( ( diff --git a/step-generation/src/__tests__/fixtureGeneration.test.ts b/step-generation/src/__tests__/fixtureGeneration.test.ts index 5f14449b473..158f0cfb6ed 100644 --- a/step-generation/src/__tests__/fixtureGeneration.test.ts +++ b/step-generation/src/__tests__/fixtureGeneration.test.ts @@ -18,6 +18,18 @@ describe('snapshot tests', () => { sourcePlateId: { slot: '4', }, + tiprack4AdapterId: { + slot: '7', + }, + tiprack5AdapterId: { + slot: '8', + }, + tiprack4Id: { + slot: 'tiprack4AdapterId', + }, + tiprack5Id: { + slot: 'tiprack5AdapterId', + }, fixedTrash: { slot: '12', }, diff --git a/step-generation/src/__tests__/moveLabware.test.ts b/step-generation/src/__tests__/moveLabware.test.ts index b57f3d7d200..12ecf2e46a8 100644 --- a/step-generation/src/__tests__/moveLabware.test.ts +++ b/step-generation/src/__tests__/moveLabware.test.ts @@ -1,6 +1,6 @@ import { HEATERSHAKER_MODULE_TYPE, - WASTE_CHUTE_SLOT, + WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import { getInitialRobotStateStandard, @@ -17,6 +17,7 @@ import { moveLabware, MoveLabwareArgs } from '..' import type { InvariantContext, RobotState } from '../types' const mockWasteChuteId = 'mockWasteChuteId' +const mockGripperId = 'mockGripperId' describe('moveLabware', () => { let robotState: RobotState @@ -24,6 +25,16 @@ describe('moveLabware', () => { beforeEach(() => { invariantContext = makeContext() robotState = getInitialRobotStateStandard(invariantContext) + + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: { + mockGripperId: { + name: 'gripper', + id: mockGripperId, + }, + }, + } }) afterEach(() => { jest.resetAllMocks() @@ -129,7 +140,7 @@ describe('moveLabware', () => { const params = { commandCreatorFnName: 'moveLabware', labware: SOURCE_LABWARE, - useGripper: true, + useGripper: false, newLocation: { moduleId: thermocyclerId }, } as MoveLabwareArgs @@ -246,10 +257,11 @@ describe('moveLabware', () => { const wasteChuteInvariantContext = { ...invariantContext, additionalEquipmentEntities: { + ...invariantContext.additionalEquipmentEntities, mockWasteChuteId: { name: 'wasteChute', id: mockWasteChuteId, - location: WASTE_CHUTE_SLOT, + location: WASTE_CHUTE_CUTOUT, }, }, } as InvariantContext @@ -266,7 +278,7 @@ describe('moveLabware', () => { commandCreatorFnName: 'moveLabware', labware: TIPRACK_1, useGripper: true, - newLocation: { slotName: WASTE_CHUTE_SLOT }, + newLocation: { addressableAreaName: 'gripperWasteChute' }, } as MoveLabwareArgs const result = moveLabware( @@ -285,10 +297,11 @@ describe('moveLabware', () => { const wasteChuteInvariantContext = { ...invariantContext, additionalEquipmentEntities: { + ...invariantContext.additionalEquipmentEntities, mockWasteChuteId: { name: 'wasteChute', id: mockWasteChuteId, - location: WASTE_CHUTE_SLOT, + location: WASTE_CHUTE_CUTOUT, }, }, } as InvariantContext @@ -304,7 +317,7 @@ describe('moveLabware', () => { commandCreatorFnName: 'moveLabware', labware: SOURCE_LABWARE, useGripper: true, - newLocation: { slotName: WASTE_CHUTE_SLOT }, + newLocation: { addressableAreaName: 'gripperWasteChute' }, } as MoveLabwareArgs const result = moveLabware( @@ -319,4 +332,49 @@ describe('moveLabware', () => { }, ]) }) + it('should return an error when trying to move with gripper when there is no gripper', () => { + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: {}, + } as InvariantContext + + const params = { + commandCreatorFnName: 'moveLabware', + labware: SOURCE_LABWARE, + useGripper: true, + newLocation: { slotName: 'A1' }, + } as MoveLabwareArgs + + const result = moveLabware(params, invariantContext, robotState) + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ + type: 'GRIPPER_REQUIRED', + }) + }) + it('should return an error when trying to move into the waste chute when useGripper is not selected', () => { + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: { + ...invariantContext.additionalEquipmentEntities, + mockWasteChuteId: { + name: 'wasteChute', + id: mockWasteChuteId, + location: WASTE_CHUTE_CUTOUT, + }, + }, + } as InvariantContext + + const params = { + commandCreatorFnName: 'moveLabware', + labware: SOURCE_LABWARE, + useGripper: false, + newLocation: { addressableAreaName: 'gripperWasteChute' }, + } as MoveLabwareArgs + + const result = moveLabware(params, invariantContext, robotState) + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ + type: 'GRIPPER_REQUIRED', + }) + }) }) diff --git a/step-generation/src/__tests__/moveToWell.test.ts b/step-generation/src/__tests__/moveToWell.test.ts index 0906d8f8629..4020cc52e08 100644 --- a/step-generation/src/__tests__/moveToWell.test.ts +++ b/step-generation/src/__tests__/moveToWell.test.ts @@ -160,6 +160,27 @@ describe('moveToWell', () => { type: 'LABWARE_OFF_DECK', }) }) + it('should return an error when dispensing from the 4th column', () => { + robotStateWithTip = { + ...robotStateWithTip, + labware: { + [SOURCE_LABWARE]: { slot: 'A4' }, + }, + } + const result = moveToWell( + { + pipette: DEFAULT_PIPETTE, + labware: SOURCE_LABWARE, + well: 'A1', + }, + invariantContext, + robotStateWithTip + ) + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ + type: 'PIPETTING_INTO_COLUMN_4', + }) + }) it('should return an error when moving to well in a thermocycler with pipette collision', () => { mockThermocyclerPipetteCollision.mockImplementationOnce( ( diff --git a/step-generation/src/__tests__/replaceTip.test.ts b/step-generation/src/__tests__/replaceTip.test.ts index c97bc008eb8..c1044d38432 100644 --- a/step-generation/src/__tests__/replaceTip.test.ts +++ b/step-generation/src/__tests__/replaceTip.test.ts @@ -7,6 +7,8 @@ import { getSuccessResult, pickUpTipHelper, dropTipHelper, + dropTipInPlaceHelper, + moveToAddressableAreaHelper, DEFAULT_PIPETTE, } from '../fixtures' import { FIXED_TRASH_ID } from '..' @@ -15,8 +17,12 @@ import type { InvariantContext, RobotState } from '../types' const tiprack1Id = 'tiprack1Id' const tiprack2Id = 'tiprack2Id' +const tiprack4Id = 'tiprack4Id' +const tiprack5Id = 'tiprack5Id' const p300SingleId = DEFAULT_PIPETTE const p300MultiId = 'p300MultiId' +const p100096Id = 'p100096Id' +const wasteChuteId = 'wasteChuteId' describe('replaceTip', () => { let invariantContext: InvariantContext let initialRobotState: RobotState @@ -132,6 +138,44 @@ describe('replaceTip', () => { }), ]) }) + it('Single-channel: dropping tips in waste chute', () => { + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: { + wasteChuteId: { + name: 'wasteChute', + id: wasteChuteId, + location: 'cutoutD3', + }, + }, + } + const initialTestRobotState = merge({}, initialRobotState, { + tipState: { + tipracks: { + [tiprack1Id]: { + A1: false, + }, + }, + pipettes: { + p300SingleId: true, + }, + }, + }) + const result = replaceTip( + { + pipette: p300SingleId, + dropTipLocation: 'wasteChuteId', + }, + invariantContext, + initialTestRobotState + ) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + moveToAddressableAreaHelper(), + dropTipInPlaceHelper(), + pickUpTipHelper('B1'), + ]) + }) }) describe('replaceTip: multi-channel', () => { it('multi-channel, all tipracks have tips', () => { @@ -205,4 +249,49 @@ describe('replaceTip', () => { ]) }) }) + describe('replaceTip: 96-channel', () => { + it('96-channel, dropping tips in waste chute', () => { + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: { + wasteChuteId: { + name: 'wasteChute', + id: wasteChuteId, + location: 'cutoutD3', + }, + }, + } + const initialTestRobotState = merge({}, initialRobotState, { + tipState: { + tipracks: { + [tiprack4Id]: getTiprackTipstate(false), + [tiprack5Id]: getTiprackTipstate(true), + }, + pipettes: { + p100096Id: true, + }, + }, + }) + const result = replaceTip( + { + pipette: p100096Id, + dropTipLocation: 'wasteChuteId', + }, + invariantContext, + initialTestRobotState + ) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + moveToAddressableAreaHelper({ + pipetteId: p100096Id, + addressableAreaName: '96ChannelWasteChute', + }), + dropTipInPlaceHelper({ pipetteId: p100096Id }), + pickUpTipHelper('A1', { + pipetteId: p100096Id, + labwareId: tiprack5Id, + }), + ]) + }) + }) }) diff --git a/step-generation/src/__tests__/transfer.test.ts b/step-generation/src/__tests__/transfer.test.ts index 82a0ae78c9b..dd7c449cd05 100644 --- a/step-generation/src/__tests__/transfer.test.ts +++ b/step-generation/src/__tests__/transfer.test.ts @@ -19,6 +19,8 @@ import { SOURCE_LABWARE, makeDispenseAirGapHelper, AIR_GAP_META, + dropTipInPlaceHelper, + moveToAddressableAreaHelper, } from '../fixtures' import { FIXED_TRASH_ID } from '..' import { @@ -109,6 +111,40 @@ describe('pick up tip if no tip on pipette', () => { expect(res.commands[0]).toEqual(pickUpTipHelper('A1')) }) }) + it('...once, drop tip in waste chute', () => { + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: { + wasteChuteId: { + name: 'wasteChute', + id: 'wasteChuteId', + location: 'cutoutD3', + }, + }, + } + + noTipArgs = { + ...noTipArgs, + changeTip: 'always', + dropTipLocation: 'wasteChuteId', + dispenseAirGapVolume: 5, + } as TransferArgs + + const result = transfer(noTipArgs, invariantContext, robotStateWithTip) + + const res = getSuccessResult(result) + + expect(res.commands).toEqual([ + pickUpTipHelper('A1'), + aspirateHelper('A1', 30), + dispenseHelper('B2', 30), + airGapHelper('B2', 5, { labwareId: 'destPlateId' }), + moveToAddressableAreaHelper({ + addressableAreaName: '1and8ChannelWasteChute', + }), + dropTipInPlaceHelper(), + ]) + }) it('...never (should not pick up tip, and fail)', () => { noTipArgs = { diff --git a/step-generation/src/__tests__/wasteChuteCommandsUtil.test.ts b/step-generation/src/__tests__/wasteChuteCommandsUtil.test.ts index b202fd2e33d..c54134c9288 100644 --- a/step-generation/src/__tests__/wasteChuteCommandsUtil.test.ts +++ b/step-generation/src/__tests__/wasteChuteCommandsUtil.test.ts @@ -1,4 +1,4 @@ -import { WASTE_CHUTE_SLOT } from '@opentrons/shared-data' +import { WASTE_CHUTE_CUTOUT } from '@opentrons/shared-data' import { getInitialRobotStateStandard, makeContext, @@ -44,7 +44,7 @@ describe('wasteChuteCommandsUtil', () => { additionalEquipmentEntities: { [mockWasteChuteId]: { name: 'wasteChute', - location: WASTE_CHUTE_SLOT, + location: WASTE_CHUTE_CUTOUT, id: 'mockId', }, }, diff --git a/step-generation/src/commandCreators/atomic/aspirate.ts b/step-generation/src/commandCreators/atomic/aspirate.ts index e32c017ef2a..efc734275f9 100644 --- a/step-generation/src/commandCreators/atomic/aspirate.ts +++ b/step-generation/src/commandCreators/atomic/aspirate.ts @@ -12,6 +12,7 @@ import { getIsHeaterShakerNorthSouthOfNonTiprackWithMultiChannelPipette, uuid, } from '../../utils' +import { COLUMN_4_SLOTS } from '../../constants' import type { CreateCommand } from '@opentrons/shared-data' import type { AspirateParams } from '@opentrons/shared-data/protocol/types/schemaV3' import type { CommandCreator, CommandCreatorError } from '../../types' @@ -64,6 +65,10 @@ export const aspirate: CommandCreator = ( errors.push(errorCreators.labwareOffDeck()) } + if (COLUMN_4_SLOTS.includes(slotName)) { + errors.push(errorCreators.pipettingIntoColumn4({ typeOfStep: actionName })) + } + if ( modulePipetteCollision({ pipette, diff --git a/step-generation/src/commandCreators/atomic/blowout.ts b/step-generation/src/commandCreators/atomic/blowout.ts index 7fbd13a56df..5c18b8d654c 100644 --- a/step-generation/src/commandCreators/atomic/blowout.ts +++ b/step-generation/src/commandCreators/atomic/blowout.ts @@ -1,8 +1,9 @@ -import { uuid } from '../../utils' +import { uuid, getLabwareSlot } from '../../utils' +import { COLUMN_4_SLOTS } from '../../constants' import * as errorCreators from '../../errorCreators' +import type { CreateCommand } from '@opentrons/shared-data' import type { BlowoutParams } from '@opentrons/shared-data/protocol/types/schemaV3' import type { CommandCreatorError, CommandCreator } from '../../types' -import { CreateCommand } from '@opentrons/shared-data' export const blowout: CommandCreator = ( args, @@ -14,7 +15,11 @@ export const blowout: CommandCreator = ( const actionName = 'blowout' const errors: CommandCreatorError[] = [] const pipetteData = prevRobotState.pipettes[pipette] - + const slotName = getLabwareSlot( + labware, + prevRobotState.labware, + prevRobotState.modules + ) // TODO Ian 2018-04-30 this logic using command creator args + robotstate to push errors // is duplicated across several command creators (eg aspirate & blowout overlap). // You can probably make higher-level error creator util fns to be more DRY @@ -49,6 +54,10 @@ export const blowout: CommandCreator = ( errors.push(errorCreators.labwareOffDeck()) } + if (COLUMN_4_SLOTS.includes(slotName)) { + errors.push(errorCreators.pipettingIntoColumn4({ typeOfStep: actionName })) + } + if (errors.length > 0) { return { errors, diff --git a/step-generation/src/commandCreators/atomic/dispense.ts b/step-generation/src/commandCreators/atomic/dispense.ts index c8ddfe1dc96..7d52d5c5eb3 100644 --- a/step-generation/src/commandCreators/atomic/dispense.ts +++ b/step-generation/src/commandCreators/atomic/dispense.ts @@ -11,6 +11,7 @@ import { getIsHeaterShakerNorthSouthOfNonTiprackWithMultiChannelPipette, uuid, } from '../../utils' +import { COLUMN_4_SLOTS } from '../../constants' import type { CreateCommand } from '@opentrons/shared-data' import type { DispenseParams } from '@opentrons/shared-data/protocol/types/schemaV3' import type { CommandCreator, CommandCreatorError } from '../../types' @@ -84,6 +85,10 @@ export const dispense: CommandCreator = ( errors.push(errorCreators.labwareOffDeck()) } + if (COLUMN_4_SLOTS.includes(slotName)) { + errors.push(errorCreators.pipettingIntoColumn4({ typeOfStep: actionName })) + } + if ( thermocyclerPipetteCollision( prevRobotState.modules, diff --git a/step-generation/src/commandCreators/atomic/moveLabware.ts b/step-generation/src/commandCreators/atomic/moveLabware.ts index 3c7c414d0e8..46962e1bb76 100644 --- a/step-generation/src/commandCreators/atomic/moveLabware.ts +++ b/step-generation/src/commandCreators/atomic/moveLabware.ts @@ -3,7 +3,6 @@ import { HEATERSHAKER_MODULE_TYPE, LabwareMovementStrategy, THERMOCYCLER_MODULE_TYPE, - WASTE_CHUTE_SLOT, } from '@opentrons/shared-data' import * as errorCreators from '../../errorCreators' import * as warningCreators from '../../warningCreators' @@ -28,6 +27,7 @@ export const moveLabware: CommandCreator = ( ) => { const { labware, useGripper, newLocation } = args const { additionalEquipmentEntities } = invariantContext + const hasWasteChute = getHasWasteChute(additionalEquipmentEntities) const tiprackHasTip = prevRobotState.tipState != null ? getTiprackHasTips(prevRobotState.tipState, labware) @@ -43,8 +43,12 @@ export const moveLabware: CommandCreator = ( const newLocationInWasteChute = newLocation !== 'offDeck' && - 'slotName' in newLocation && - newLocation.slotName === WASTE_CHUTE_SLOT + 'addressableAreaName' in newLocation && + newLocation.addressableAreaName === 'gripperWasteChute' + + const hasGripper = Object.values(additionalEquipmentEntities).find( + aE => aE.name === 'gripper' + ) if (!labware || !prevRobotState.labware[labware]) { errors.push( @@ -57,6 +61,13 @@ export const moveLabware: CommandCreator = ( errors.push(errorCreators.labwareOffDeck()) } + if ( + (newLocationInWasteChute && hasGripper && !useGripper) || + (!hasGripper && useGripper) + ) { + errors.push(errorCreators.gripperRequired()) + } + const initialLabwareSlot = prevRobotState.labware[labware]?.slot const initialAdapterSlot = prevRobotState.labware[initialLabwareSlot]?.slot const initialSlot = @@ -87,45 +98,42 @@ export const moveLabware: CommandCreator = ( newLocation !== 'offDeck' && 'labwareId' in newLocation ? newLocation.labwareId : null - const destModuleIdUnderAdapter = + + const destModuleOrSlotUnderAdapterId = destAdapterId != null ? prevRobotState.labware[destAdapterId].slot : null - const destinationModuleId = - destModuleIdUnderAdapter != null ? destModuleIdUnderAdapter : destModuleId + const destinationModuleIdOrSlot = + destModuleOrSlotUnderAdapterId != null + ? destModuleOrSlotUnderAdapterId + : destModuleId if (newLocation === 'offDeck' && useGripper) { errors.push(errorCreators.labwareOffDeck()) } - if ( - tiprackHasTip && - newLocationInWasteChute && - getHasWasteChute(additionalEquipmentEntities) - ) { + if (tiprackHasTip && newLocationInWasteChute && hasWasteChute) { warnings.push(warningCreators.tiprackInWasteChuteHasTips()) - } else if ( - labwareHasLiquid && - newLocationInWasteChute && - getHasWasteChute(additionalEquipmentEntities) - ) { + } else if (labwareHasLiquid && newLocationInWasteChute && hasWasteChute) { warnings.push(warningCreators.labwareInWasteChuteHasLiquid()) } - if (destinationModuleId != null) { + if ( + destinationModuleIdOrSlot != null && + prevRobotState.modules[destinationModuleIdOrSlot] != null + ) { const destModuleState = - prevRobotState.modules[destinationModuleId]?.moduleState ?? null - if (destModuleState != null) { - if ( - destModuleState.type === THERMOCYCLER_MODULE_TYPE && - destModuleState.lidOpen !== true - ) { - errors.push(errorCreators.thermocyclerLidClosed()) - } else if (destModuleState.type === HEATERSHAKER_MODULE_TYPE) { - if (destModuleState.latchOpen !== true) { - errors.push(errorCreators.heaterShakerLatchClosed()) - } - if (destModuleState.targetSpeed !== null) { - errors.push(errorCreators.heaterShakerIsShaking()) - } + prevRobotState.modules[destinationModuleIdOrSlot].moduleState + + if ( + destModuleState.type === THERMOCYCLER_MODULE_TYPE && + destModuleState.lidOpen !== true + ) { + errors.push(errorCreators.thermocyclerLidClosed()) + } else if (destModuleState.type === HEATERSHAKER_MODULE_TYPE) { + if (destModuleState.latchOpen !== true) { + errors.push(errorCreators.heaterShakerLatchClosed()) + } + if (destModuleState.targetSpeed !== null) { + errors.push(errorCreators.heaterShakerIsShaking()) } } } @@ -149,6 +157,7 @@ export const moveLabware: CommandCreator = ( params, }, ] + return { commands, warnings: warnings.length > 0 ? warnings : undefined, diff --git a/step-generation/src/commandCreators/atomic/moveToWell.ts b/step-generation/src/commandCreators/atomic/moveToWell.ts index bf2369509cc..e16f1cff417 100644 --- a/step-generation/src/commandCreators/atomic/moveToWell.ts +++ b/step-generation/src/commandCreators/atomic/moveToWell.ts @@ -11,6 +11,7 @@ import { getIsHeaterShakerNorthSouthOfNonTiprackWithMultiChannelPipette, uuid, } from '../../utils' +import { COLUMN_4_SLOTS } from '../../constants' import type { CreateCommand } from '@opentrons/shared-data' import type { MoveToWellParams as v5MoveToWellParams } from '@opentrons/shared-data/protocol/types/schemaV5' import type { MoveToWellParams as v6MoveToWellParams } from '@opentrons/shared-data/protocol/types/schemaV6/command/gantry' @@ -58,6 +59,12 @@ export const moveToWell: CommandCreator = ( errors.push(errorCreators.labwareOffDeck()) } + if (COLUMN_4_SLOTS.includes(slotName)) { + errors.push( + errorCreators.pipettingIntoColumn4({ typeOfStep: 'move to well' }) + ) + } + if ( modulePipetteCollision({ pipette, diff --git a/step-generation/src/commandCreators/atomic/replaceTip.ts b/step-generation/src/commandCreators/atomic/replaceTip.ts index cf59d1341d2..55e3348b78d 100644 --- a/step-generation/src/commandCreators/atomic/replaceTip.ts +++ b/step-generation/src/commandCreators/atomic/replaceTip.ts @@ -1,5 +1,6 @@ import { getNextTiprack } from '../../robotStateSelectors' import * as errorCreators from '../../errorCreators' +import { COLUMN_4_SLOTS } from '../../constants' import { dropTip } from './dropTip' import { curryCommandCreator, @@ -10,6 +11,7 @@ import { pipetteAdjacentHeaterShakerWhileShaking, getIsHeaterShakerEastWestWithLatchOpen, getIsHeaterShakerEastWestMultiChannelPipette, + wasteChuteCommandsUtil, } from '../../utils' import type { CommandCreatorError, @@ -37,6 +39,11 @@ const _pickUpTip: CommandCreator = ( if (adapterId == null && pipetteName === 'p1000_96') { errors.push(errorCreators.missingAdapter()) } + if (COLUMN_4_SLOTS.includes(tiprackSlot)) { + errors.push( + errorCreators.pipettingIntoColumn4({ typeOfStep: 'pick up tip' }) + ) + } if (errors.length > 0) { return { errors } @@ -98,6 +105,9 @@ export const replaceTip: CommandCreator = ( const labwareDef = invariantContext.labwareEntities[nextTiprack.tiprackId]?.def + const isWasteChute = + invariantContext.additionalEquipmentEntities[dropTipLocation] != null + if (!labwareDef) { return { errors: [ @@ -158,17 +168,35 @@ export const replaceTip: CommandCreator = ( } } - const commandCreators: CurriedCommandCreator[] = [ - curryCommandCreator(dropTip, { - pipette, - dropTipLocation, - }), - curryCommandCreator(_pickUpTip, { - pipette, - tiprack: nextTiprack.tiprackId, - well: nextTiprack.well, - }), - ] + const addressableAreaName = + pipetteSpec.channels === 96 + ? '96ChannelWasteChute' + : '1and8ChannelWasteChute' + + const commandCreators: CurriedCommandCreator[] = isWasteChute + ? [ + curryCommandCreator(wasteChuteCommandsUtil, { + type: 'dropTip', + pipetteId: pipette, + addressableAreaName, + }), + curryCommandCreator(_pickUpTip, { + pipette, + tiprack: nextTiprack.tiprackId, + well: nextTiprack.well, + }), + ] + : [ + curryCommandCreator(dropTip, { + pipette, + dropTipLocation, + }), + curryCommandCreator(_pickUpTip, { + pipette, + tiprack: nextTiprack.tiprackId, + well: nextTiprack.well, + }), + ] return reduceCommandCreators( commandCreators, diff --git a/step-generation/src/commandCreators/compound/consolidate.ts b/step-generation/src/commandCreators/compound/consolidate.ts index d07841cb579..5908d96bdc0 100644 --- a/step-generation/src/commandCreators/compound/consolidate.ts +++ b/step-generation/src/commandCreators/compound/consolidate.ts @@ -8,6 +8,7 @@ import { blowoutUtil, curryCommandCreator, reduceCommandCreators, + wasteChuteCommandsUtil, } from '../../utils' import { configureForVolume } from '../atomic/configureForVolume' import { @@ -96,6 +97,25 @@ export const consolidate: CommandCreator = ( const sourceWellChunks = chunk(args.sourceWells, maxWellsPerChunk) + const isWasteChute = + invariantContext.additionalEquipmentEntities[args.dropTipLocation] != null + + const addressableAreaName = + invariantContext.pipetteEntities[args.pipette].spec.channels === 96 + ? '96ChannelWasteChute' + : '1and8ChannelWasteChute' + + const dropTipCommand = isWasteChute + ? curryCommandCreator(wasteChuteCommandsUtil, { + type: 'dropTip', + pipetteId: args.pipette, + addressableAreaName, + }) + : curryCommandCreator(dropTip, { + pipette: args.pipette, + dropTipLocation: args.dropTipLocation, + }) + const commandCreators = flatMap( sourceWellChunks, ( @@ -302,14 +322,7 @@ export const consolidate: CommandCreator = ( : [] // if using dispense > air gap, drop or change the tip at the end const dropTipAfterDispenseAirGap = - airGapAfterDispenseCommands.length > 0 - ? [ - curryCommandCreator(dropTip, { - pipette: args.pipette, - dropTipLocation: dropTipLocation, - }), - ] - : [] + airGapAfterDispenseCommands.length > 0 ? [dropTipCommand] : [] const blowoutCommand = blowoutUtil({ pipette: args.pipette, sourceLabwareId: args.sourceLabware, diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index 962885376b1..6715cab5b14 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -9,6 +9,7 @@ import { curryCommandCreator, getDispenseAirGapLocation, reduceCommandCreators, + wasteChuteCommandsUtil, } from '../../utils' import { aspirate, @@ -91,6 +92,26 @@ export const transfer: CommandCreator = ( errors, } const pipetteSpec = invariantContext.pipetteEntities[args.pipette].spec + + const isWasteChute = + invariantContext.additionalEquipmentEntities[args.dropTipLocation] != null + + const addressableAreaName = + pipetteSpec.channels === 96 + ? '96ChannelWasteChute' + : '1and8ChannelWasteChute' + + const dropTipCommand = isWasteChute + ? curryCommandCreator(wasteChuteCommandsUtil, { + type: 'dropTip', + pipetteId: args.pipette, + addressableAreaName: addressableAreaName, + }) + : curryCommandCreator(dropTip, { + pipette: args.pipette, + dropTipLocation: args.dropTipLocation, + }) + // TODO: BC 2019-07-08 these argument names are a bit misleading, instead of being values bound // to the action of aspiration of dispensing in a given command, they are actually values bound // to a given labware associated with a command (e.g. Source, Destination). For this reason we @@ -407,12 +428,7 @@ export const transfer: CommandCreator = ( // if using dispense > air gap, drop or change the tip at the end const dropTipAfterDispenseAirGap = airGapAfterDispenseCommands.length > 0 && isLastChunk && isLastPair - ? [ - curryCommandCreator(dropTip, { - pipette: args.pipette, - dropTipLocation: args.dropTipLocation, - }), - ] + ? [dropTipCommand] : [] const blowoutCommand = blowoutUtil({ pipette: args.pipette, diff --git a/step-generation/src/constants.ts b/step-generation/src/constants.ts index 17e278eadc7..63e0f0d4018 100644 --- a/step-generation/src/constants.ts +++ b/step-generation/src/constants.ts @@ -2,6 +2,8 @@ import { MAGNETIC_MODULE_V1, TEMPERATURE_MODULE_V1, } from '@opentrons/shared-data' +import type { ModuleModel } from '@opentrons/shared-data' + // Temperature statuses export const TEMPERATURE_DEACTIVATED: 'TEMPERATURE_DEACTIVATED' = 'TEMPERATURE_DEACTIVATED' @@ -10,7 +12,7 @@ export const TEMPERATURE_AT_TARGET: 'TEMPERATURE_AT_TARGET' = export const TEMPERATURE_APPROACHING_TARGET: 'TEMPERATURE_APPROACHING_TARGET' = 'TEMPERATURE_APPROACHING_TARGET' export const AIR_GAP_OFFSET_FROM_TOP = 1 -export const MODULES_WITH_COLLISION_ISSUES = [ +export const MODULES_WITH_COLLISION_ISSUES: ModuleModel[] = [ MAGNETIC_MODULE_V1, TEMPERATURE_MODULE_V1, ] @@ -18,3 +20,5 @@ export const FIXED_TRASH_ID: 'fixedTrash' = 'fixedTrash' export const OT_2_TRASH_DEF_URI = 'opentrons/opentrons_1_trash_1100ml_fixed/1' export const FLEX_TRASH_DEF_URI = 'opentrons/opentrons_1_trash_3200ml_fixed/1' + +export const COLUMN_4_SLOTS = ['A4', 'B4', 'C4', 'D4'] diff --git a/step-generation/src/errorCreators.ts b/step-generation/src/errorCreators.ts index be1407c2b97..3d6cadc5c57 100644 --- a/step-generation/src/errorCreators.ts +++ b/step-generation/src/errorCreators.ts @@ -211,3 +211,19 @@ export const additionalEquipmentDoesNotExist = (args: { message: `The ${args.additionalEquipment} does not exist`, } } + +export const gripperRequired = (): CommandCreatorError => { + return { + type: 'GRIPPER_REQUIRED', + message: 'The gripper is required to fulfill this action', + } +} + +export const pipettingIntoColumn4 = (args: { + typeOfStep: string +}): CommandCreatorError => { + return { + type: 'PIPETTING_INTO_COLUMN_4', + message: `Cannot ${args.typeOfStep} into a column 4 slot.`, + } +} diff --git a/step-generation/src/fixtures/commandFixtures.ts b/step-generation/src/fixtures/commandFixtures.ts index b3e881fa5e9..2261323f430 100644 --- a/step-generation/src/fixtures/commandFixtures.ts +++ b/step-generation/src/fixtures/commandFixtures.ts @@ -90,6 +90,7 @@ export const getFlowRateAndOffsetParamsMix = (): FlowRateAndOffsetParamsMix => ( // ================= export const DEFAULT_PIPETTE = 'p300SingleId' export const MULTI_PIPETTE = 'p300MultiId' +export const PIPETTE_96 = 'p100096Id' export const SOURCE_LABWARE = 'sourcePlateId' export const DEST_LABWARE = 'destPlateId' export const TROUGH_LABWARE = 'troughId' @@ -310,3 +311,25 @@ export const pickUpTipHelper = ( wellName: typeof tip === 'string' ? tip : tiprackWellNamesFlat[tip], }, }) +export const dropTipInPlaceHelper = (params?: { + pipetteId?: string +}): CreateCommand => ({ + commandType: 'dropTipInPlace', + key: expect.any(String), + params: { + pipetteId: DEFAULT_PIPETTE, + ...params, + }, +}) +export const moveToAddressableAreaHelper = (params?: { + pipetteId?: string + addressableAreaName: string +}): CreateCommand => ({ + commandType: 'moveToAddressableArea', + key: expect.any(String), + params: { + pipetteId: DEFAULT_PIPETTE, + addressableAreaName: '1and8ChannelWasteChute', + ...params, + }, +}) diff --git a/step-generation/src/fixtures/robotStateFixtures.ts b/step-generation/src/fixtures/robotStateFixtures.ts index aa44e110dd3..42b1fa7fa58 100644 --- a/step-generation/src/fixtures/robotStateFixtures.ts +++ b/step-generation/src/fixtures/robotStateFixtures.ts @@ -10,12 +10,15 @@ import { fixtureP10Multi as _fixtureP10Multi, fixtureP300Single as _fixtureP300Single, fixtureP300Multi as _fixtureP300Multi, + fixtureP100096 as _fixtureP100096, } from '@opentrons/shared-data/pipette/fixtures/name' import _fixtureTrash from '@opentrons/shared-data/labware/fixtures/2/fixture_trash.json' import _fixture96Plate from '@opentrons/shared-data/labware/fixtures/2/fixture_96_plate.json' import _fixture12Trough from '@opentrons/shared-data/labware/fixtures/2/fixture_12_trough.json' import _fixtureTiprack10ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_10_ul.json' import _fixtureTiprack300ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' +import _fixtureTiprack1000ul from '@opentrons/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_1000ul.json' +import _fixtureTiprackAdapter from '@opentrons/shared-data/labware/fixtures/2/fixture_flex_96_tiprack_adapter.json' import { TEMPERATURE_APPROACHING_TARGET, TEMPERATURE_AT_TARGET, @@ -26,6 +29,7 @@ import { import { DEFAULT_PIPETTE, MULTI_PIPETTE, + PIPETTE_96, SOURCE_LABWARE, DEST_LABWARE, TROUGH_LABWARE, @@ -47,12 +51,15 @@ const fixtureP10Single = _fixtureP10Single const fixtureP10Multi = _fixtureP10Multi const fixtureP300Single = _fixtureP300Single const fixtureP300Multi = _fixtureP300Multi +const fixtureP100096 = _fixtureP100096 const fixtureTrash = _fixtureTrash as LabwareDefinition2 const fixture96Plate = _fixture96Plate as LabwareDefinition2 const fixture12Trough = _fixture12Trough as LabwareDefinition2 const fixtureTiprack10ul = _fixtureTiprack10ul as LabwareDefinition2 const fixtureTiprack300ul = _fixtureTiprack300ul as LabwareDefinition2 +const fixtureTiprack1000ul = _fixtureTiprack1000ul as LabwareDefinition2 +const fixtureTiprackAdapter = _fixtureTiprackAdapter as LabwareDefinition2 export const DEFAULT_CONFIG: Config = { OT_PD_DISABLE_MODULE_RESTRICTIONS: false, @@ -120,6 +127,30 @@ export function makeContext(): InvariantContext { labwareDefURI: getLabwareDefURI(fixtureTiprack300ul), def: fixtureTiprack300ul, }, + tiprack4AdapterId: { + id: 'tiprack4AdapterId', + + labwareDefURI: getLabwareDefURI(fixtureTiprackAdapter), + def: fixtureTiprackAdapter, + }, + tiprack5AdapterId: { + id: 'tiprack5AdapterId', + + labwareDefURI: getLabwareDefURI(fixtureTiprackAdapter), + def: fixtureTiprackAdapter, + }, + tiprack4Id: { + id: 'tiprack4Id', + + labwareDefURI: getLabwareDefURI(fixtureTiprack1000ul), + def: fixtureTiprack1000ul, + }, + tiprack5Id: { + id: 'tiprack5Id', + + labwareDefURI: getLabwareDefURI(fixtureTiprack1000ul), + def: fixtureTiprack1000ul, + }, } const moduleEntities: ModuleEntities = {} const additionalEquipmentEntities: AdditionalEquipmentEntities = {} @@ -156,6 +187,14 @@ export function makeContext(): InvariantContext { tiprackLabwareDef: fixtureTiprack300ul, spec: fixtureP300Multi, }, + [PIPETTE_96]: { + name: 'p1000_96', + id: PIPETTE_96, + + tiprackDefURI: getLabwareDefURI(fixtureTiprack1000ul), + tiprackLabwareDef: fixtureTiprack1000ul, + spec: fixtureP100096, + }, } return { labwareEntities, @@ -213,6 +252,18 @@ export const makeStateArgsStandard = (): StandardMakeStateArgs => ({ tiprack2Id: { slot: '5', }, + tiprack4AdapterId: { + slot: '7', + }, + tiprack5AdapterId: { + slot: '8', + }, + tiprack4Id: { + slot: 'tiprack4AdapterId', + }, + tiprack5Id: { + slot: 'tiprack5AdapterId', + }, sourcePlateId: { slot: '2', }, diff --git a/step-generation/src/getNextRobotStateAndWarnings/forMoveLabware.ts b/step-generation/src/getNextRobotStateAndWarnings/forMoveLabware.ts index 56ddf85115e..389860d302b 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/forMoveLabware.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/forMoveLabware.ts @@ -18,6 +18,8 @@ export function forMoveLabware( newLocationString = newLocation.slotName } else if ('labwareId' in newLocation) { newLocationString = newLocation.labwareId + } else if ('addressableAreaName' in newLocation) { + newLocationString = newLocation.addressableAreaName } robotState.labware[labwareId].slot = newLocationString diff --git a/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts b/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts index 19a20c04db3..19028db007f 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/inPlaceCommandUpdates.ts @@ -24,5 +24,7 @@ export const forDropTipInPlace = ( invariantContext: InvariantContext, robotStateAndWarnings: RobotStateAndWarnings ): void => { - // TODO(jr, 11/6/23): update state + const { pipetteId } = params + const { robotState } = robotStateAndWarnings + robotState.tipState.pipettes[pipetteId] = false } diff --git a/step-generation/src/types.ts b/step-generation/src/types.ts index 4d5fdfe5340..26dbf13368a 100644 --- a/step-generation/src/types.ts +++ b/step-generation/src/types.ts @@ -492,6 +492,7 @@ export interface RobotState { export type ErrorType = | 'ADDITIONAL_EQUIPMENT_DOES_NOT_EXIST' | 'DROP_TIP_LOCATION_DOES_NOT_EXIST' + | 'GRIPPER_REQUIRED' | 'HEATER_SHAKER_EAST_WEST_LATCH_OPEN' | 'HEATER_SHAKER_EAST_WEST_MULTI_CHANNEL' | 'HEATER_SHAKER_IS_SHAKING' @@ -511,6 +512,7 @@ export type ErrorType = | 'NO_TIP_ON_PIPETTE' | 'PIPETTE_DOES_NOT_EXIST' | 'PIPETTE_VOLUME_EXCEEDED' + | 'PIPETTING_INTO_COLUMN_4' | 'TALL_LABWARE_EAST_WEST_OF_HEATER_SHAKER' | 'THERMOCYCLER_LID_CLOSED' | 'TIP_VOLUME_EXCEEDED' diff --git a/step-generation/src/utils/misc.ts b/step-generation/src/utils/misc.ts index 8dd2df669ff..0946cea97ee 100644 --- a/step-generation/src/utils/misc.ts +++ b/step-generation/src/utils/misc.ts @@ -8,7 +8,7 @@ import { getLabwareDefURI, getWellsDepth, getWellNamePerMultiTip, - WASTE_CHUTE_SLOT, + WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import { blowout } from '../commandCreators/atomic/blowout' import { curryCommandCreator } from './curryCommandCreator' @@ -343,7 +343,7 @@ export const getHasWasteChute = ( ): boolean => { return Object.values(additionalEquipmentEntities).some( additionalEquipmentEntity => - additionalEquipmentEntity.location === WASTE_CHUTE_SLOT && + additionalEquipmentEntity.location === WASTE_CHUTE_CUTOUT && additionalEquipmentEntity.name === 'wasteChute' ) } diff --git a/step-generation/src/utils/modulePipetteCollision.ts b/step-generation/src/utils/modulePipetteCollision.ts index 745becfbe02..2ab09bbb109 100644 --- a/step-generation/src/utils/modulePipetteCollision.ts +++ b/step-generation/src/utils/modulePipetteCollision.ts @@ -36,7 +36,6 @@ export const modulePipetteCollision = (args: { const labwareInDangerZone = Object.keys(invariantContext.moduleEntities).some( moduleId => { const moduleModel = invariantContext.moduleEntities[moduleId].model - // @ts-expect-error(SA, 2021-05-03): need to type narrow if (MODULES_WITH_COLLISION_ISSUES.includes(moduleModel)) { const moduleSlot: DeckSlot | null | undefined = prevRobotState.modules[moduleId]?.slot diff --git a/step-generation/src/utils/wasteChuteCommandsUtil.ts b/step-generation/src/utils/wasteChuteCommandsUtil.ts index 29df4f550ca..1473026450d 100644 --- a/step-generation/src/utils/wasteChuteCommandsUtil.ts +++ b/step-generation/src/utils/wasteChuteCommandsUtil.ts @@ -62,15 +62,18 @@ export const wasteChuteCommandsUtil: CommandCreator = ( let commands: CurriedCommandCreator[] = [] switch (type) { case 'dropTip': { - commands = [ - curryCommandCreator(moveToAddressableArea, { - pipetteId, - addressableAreaName, - }), - curryCommandCreator(dropTipInPlace, { - pipetteId, - }), - ] + commands = !prevRobotState.tipState.pipettes[pipetteId] + ? [] + : [ + curryCommandCreator(moveToAddressableArea, { + pipetteId, + addressableAreaName, + }), + curryCommandCreator(dropTipInPlace, { + pipetteId, + }), + ] + break } case 'dispense': { diff --git a/tsconfig-eslint.json b/tsconfig-eslint.json index 555566fb1f5..11672e612c6 100644 --- a/tsconfig-eslint.json +++ b/tsconfig-eslint.json @@ -19,6 +19,7 @@ "labware-designer/typings", "labware-library/src", "labware-library/typings", + "shared-data/deck", "shared-data/js", "shared-data/protocol", "shared-data/pipette",