Skip to content

Commit

Permalink
feat(api, app): Check Robot Deck Transform (#5845)
Browse files Browse the repository at this point in the history
  • Loading branch information
Laura-Danielle authored Jun 18, 2020
1 parent 78c3ebb commit ed67383
Show file tree
Hide file tree
Showing 17 changed files with 384 additions and 23 deletions.
45 changes: 44 additions & 1 deletion api/src/opentrons/hardware_control/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
import contextlib
import logging
import pathlib
import numpy as np # type: ignore
from collections import OrderedDict
from typing import Dict, Union, List, Optional, Tuple, TYPE_CHECKING

from opentrons_shared_data.pipette import name_for_model

from opentrons import types as top_types
from opentrons.util import linal
from functools import lru_cache
from opentrons.config import robot_configs, pipette_config
from opentrons.drivers.types import MoveSplit

from .util import use_or_initialize_loop
from .util import use_or_initialize_loop, DeckTransformState
from .pipette import Pipette
from .controller import Controller
from .simulator import Simulator
Expand Down Expand Up @@ -219,6 +221,45 @@ def is_simulator(self):
""" `True` if this is a simulator; `False` otherwise. """
return isinstance(self._backend, Simulator)

def validate_calibration(self) -> DeckTransformState:
"""
The lru cache decorator is currently not supported by the
ThreadManager. To work around this, we need to wrap the
actualy function around a dummy outer function.
Once decorators are more fully supported, we can remove this.
"""
return self._calculate_valid_calibration()

@lru_cache(maxsize=1)
def _calculate_valid_calibration(self) -> DeckTransformState:
"""
This function determines whether the current gantry
calibration is valid or not based on the following use-cases:
"""
curr_cal = np.array(self._config.gantry_calibration)
row, col = curr_cal.shape
rank = np.linalg.matrix_rank(curr_cal)

id_matrix = linal.identity_deck_transform()

z = abs(curr_cal[2][-1])

outofrange = z < 16 or z > 34
if row != rank:
# Check that the matrix is non-singular
return DeckTransformState.SINGULARITY
elif np.array_equal(curr_cal, id_matrix):
# Check that the matrix is not an identity
return DeckTransformState.IDENTITY
elif outofrange:
# Check that the matrix is not out of range.
return DeckTransformState.BAD_CALIBRATION
else:
# Transform as it stands is sufficient.
return DeckTransformState.OK

async def register_callback(self, cb):
""" Allows the caller to register a callback, and returns a closure
that can be used to unregister the provided callback
Expand Down Expand Up @@ -1000,6 +1041,8 @@ async def update_config(self, **kwargs):
Documentation on keys can be found in the documentation for
:py:class:`.robot_config`.
"""
if kwargs.get('gantry_calibration'):
self._calculate_valid_calibration.cache_clear()
self._config = self._config._replace(**kwargs) # type: ignore

async def update_deck_calibration(self, new_transform):
Expand Down
2 changes: 1 addition & 1 deletion api/src/opentrons/hardware_control/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def __init__(
requesting instruments that _are_
present get the full number.
"""
self._config = config
self.config = config
self._loop = loop

def _sanitize_attached_instrument(
Expand Down
3 changes: 3 additions & 0 deletions api/src/opentrons/hardware_control/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ def door_state(self) -> DoorState:
def door_state(self, door_state: DoorState) -> DoorState:
...

def validate_calibration(self):
...


class BoardRevision(enum.Enum):
UNKNOWN = enum.auto()
Expand Down
11 changes: 11 additions & 0 deletions api/src/opentrons/hardware_control/util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
""" Utility functions and classes for the hardware controller"""
import asyncio
import logging
from enum import Enum
from typing import Dict, Any, Optional, List, Tuple

from .types import CriticalPoint
Expand Down Expand Up @@ -37,3 +38,13 @@ def plan_arc(
for wp in checked_wp]\
+ [(dest_point._replace(z=z_height), dest_cp),
(dest_point, dest_cp)]


class DeckTransformState(Enum):
OK = "OK"
IDENTITY = "IDENTITY"
BAD_CALIBRATION = "BAD_CALIBRATION"
SINGULARITY = "SINGULARITY"

def __str__(self):
return self.name
31 changes: 31 additions & 0 deletions api/tests/opentrons/hardware_control/test_api_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from opentrons.hardware_control.util import DeckTransformState


async def test_validating_calibration(hardware):

singular_matrix = [
[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]]

await hardware.update_config(gantry_calibration=singular_matrix)

assert hardware.validate_calibration() == DeckTransformState.SINGULARITY

identity_matrix = [
[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
await hardware.update_config(gantry_calibration=identity_matrix)

assert hardware.validate_calibration() == DeckTransformState.IDENTITY

outofrange_matrix = [
[1, 0, 0, 5], [0, 1, 0, 4], [0, 0, 1, 0], [0, 0, 0, 1]]
await hardware.update_config(gantry_calibration=outofrange_matrix)

assert hardware.validate_calibration() ==\
DeckTransformState.BAD_CALIBRATION

inrange_matrix = [
[1, 0, 0, 1], [0, 1, 0, 2], [0, 0, 1, -25], [0, 0, 0, 1]]
await hardware.update_config(gantry_calibration=inrange_matrix)
hardware.validate_calibration()

assert hardware.validate_calibration() == DeckTransformState.OK
19 changes: 19 additions & 0 deletions app/src/components/RobotSettings/CheckCalibrationControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import {
COLOR_WARNING,
FONT_SIZE_BODY_1,
FONT_WEIGHT_SEMIBOLD,
Tooltip,
useHoverTooltip,
TOOLTIP_BOTTOM,
TOOLTIP_FIXED,
} from '@opentrons/components'

import { Portal } from '../portal'
Expand All @@ -39,6 +43,9 @@ const COULD_NOT_START = 'Could not start Robot Calibration Check'
const PLEASE_TRY_AGAIN =
'Please try again or contact support if you continue to experience issues'

const DECK_CAL_TOOL_TIP_MESSAGE =
'Perform a deck calibration to enable this feature.'

export function CheckCalibrationControl({
robotName,
disabled,
Expand All @@ -65,11 +72,19 @@ export function CheckCalibrationControl({
<Icon name="ot-spinner" height="1em" spin />
)

const [targetProps, tooltipProps] = useHoverTooltip({
placement: TOOLTIP_BOTTOM,
strategy: TOOLTIP_FIXED,
})

const calCheckDisabled = false

React.useEffect(() => {
if (requestStatus === RobotApi.SUCCESS) setShowWizard(true)
}, [requestStatus])

// TODO(mc, 2020-06-17): extract alert presentational stuff
// TODO(lc, 2020-06-18): edit calCheckDisabled to check for a bad calibration status from the new endpoint
return (
<>
<TitledButton
Expand All @@ -80,8 +95,12 @@ export function CheckCalibrationControl({
children: buttonChildren,
disabled: buttonDisabled,
onClick: ensureSession,
hoverToolTipHandler: targetProps,
}}
>
{calCheckDisabled && (
<Tooltip {...tooltipProps}>{DECK_CAL_TOOL_TIP_MESSAGE}</Tooltip>
)}
{requestState && requestState.status === RobotApi.FAILURE && (
<Flex
alignItems={ALIGN_CENTER}
Expand Down
35 changes: 24 additions & 11 deletions app/src/components/RobotSettings/ControlsCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import * as React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { push } from 'connected-react-router'
import { Card, LabeledToggle, LabeledButton } from '@opentrons/components'
import {
Card,
LabeledToggle,
LabeledButton,
BORDER_SOLID_LIGHT,
} from '@opentrons/components'

import { startDeckCalibration } from '../../http-api-client'
import { getFeatureFlags } from '../../config'
Expand All @@ -21,7 +26,9 @@ import { CONNECTABLE } from '../../discovery'

import type { State, Dispatch } from '../../types'
import type { ViewableRobot } from '../../discovery/types'
import { TitledButton } from '../TitledButton'
import { CheckCalibrationControl } from './CheckCalibrationControl'
import { DeckCalibrationWarning } from './DeckCalibrationWarning'

type Props = {|
robot: ViewableRobot,
Expand Down Expand Up @@ -56,25 +63,24 @@ export function ControlsCard(props: Props): React.Node {
}, [dispatch, robotName])

const buttonDisabled = notConnectable || !canControl
const calCheckDisabled = buttonDisabled || false

const deckTransformStatus = 'OK'

return (
<Card title={TITLE} disabled={notConnectable}>
{ff.enableRobotCalCheck && (
<CheckCalibrationControl
robotName={robotName}
disabled={buttonDisabled}
/>
)}
<LabeledButton
label="Calibrate deck"
<TitledButton
borderBottom={BORDER_SOLID_LIGHT}
title="Calibrate deck"
description={CALIBRATE_DECK_DESCRIPTION}
buttonProps={{
onClick: startCalibration,
disabled: buttonDisabled,
children: 'Calibrate',
}}
>
<p>{CALIBRATE_DECK_DESCRIPTION}</p>
</LabeledButton>
<DeckCalibrationWarning calibrationStatus={deckTransformStatus} />
</TitledButton>
<LabeledButton
label="Home all axes"
buttonProps={{
Expand Down Expand Up @@ -102,6 +108,13 @@ export function ControlsCard(props: Props): React.Node {
>
<p>Control lights on deck.</p>
</LabeledToggle>

{ff.enableRobotCalCheck && (
<CheckCalibrationControl
robotName={robotName}
disabled={calCheckDisabled}
/>
)}
</Card>
)
}
54 changes: 54 additions & 0 deletions app/src/components/RobotSettings/DeckCalibrationWarning.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// @flow
import * as React from 'react'
import {
Icon,
Box,
Flex,
Text,
FONT_SIZE_BODY_1,
COLOR_WARNING,
COLOR_ERROR,
SPACING_AUTO,
SPACING_1,
ALIGN_CENTER,
} from '@opentrons/components'

import styles from './styles.css'

export type DeckCalibrationWarningProps = {|
calibrationStatus: string,
|}

const ROBOT_CAL_WARNING = "This robot's deck has not yet been calibrated."
const ROBOT_CAL_RESOLUTION =
'Please perform a deck calibration prior to uploading a protocol.'
const ROBOT_CAL_ERROR =
'Bad deck calibration detected! This robot is likely to experience a crash.'

export function DeckCalibrationWarning({
calibrationStatus,
}: DeckCalibrationWarningProps): React.Node {
const isVisible = calibrationStatus !== 'OK'
const isNoCalibration = calibrationStatus === 'IDENTITY'
const message = isNoCalibration ? ROBOT_CAL_WARNING : ROBOT_CAL_ERROR
const colorType = isNoCalibration ? COLOR_WARNING : COLOR_ERROR
const styleType = isNoCalibration
? styles.cal_check_warning_icon
: styles.cal_check_error_icon

if (!isVisible) return null

return (
<Flex alignItems={ALIGN_CENTER}>
<Icon name={'alert-circle'} className={styleType} />
<Box fontSize={FONT_SIZE_BODY_1} paddingRight={SPACING_1}>
<Text color={colorType} marginRight={SPACING_AUTO}>
{message}
</Text>
<Text color={colorType} marginRight={SPACING_AUTO}>
{ROBOT_CAL_RESOLUTION}
</Text>
</Box>
</Flex>
)
}
23 changes: 19 additions & 4 deletions app/src/components/RobotSettings/__tests__/ControlsCard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ControlsCard } from '../ControlsCard'
import { CheckCalibrationControl } from '../CheckCalibrationControl'
import { LabeledToggle, LabeledButton } from '@opentrons/components'
import { CONNECTABLE, UNREACHABLE } from '../../../discovery'
import { DeckCalibrationWarning } from '../DeckCalibrationWarning'

import type { State } from '../../../types'
import type { ViewableRobot } from '../../../discovery/types'
Expand Down Expand Up @@ -55,10 +56,7 @@ describe('ControlsCard', () => {
let render

const getDeckCalButton = wrapper =>
wrapper
.find({ label: 'Calibrate deck' })
.find(LabeledButton)
.find('button')
wrapper.find('TitledButton[title="Calibrate deck"]').find('button')

const getCheckCalibrationControl = wrapper =>
wrapper.find(CheckCalibrationControl)
Expand Down Expand Up @@ -203,4 +201,21 @@ describe('ControlsCard', () => {

expect(wrapper.exists(CheckCalibrationControl)).toBe(false)
})

it('Check cal button is disabled if deck calibration is bad', () => {
const wrapper = render()

// TODO(lc, 2020-06-18): Mock out the new transform status such that
// this should evaluate to true.
expect(getCheckCalibrationControl(wrapper).prop('disabled')).toBe(false)
})

it('DeckCalibrationWarning component renders if deck calibration is bad', () => {
const wrapper = render()

// check that the deck calibration warning component is not null
// TODO(lc, 2020-06-18): Mock out the new transform status such that
// this should evaluate to true.
expect(wrapper.exists(DeckCalibrationWarning)).toBe(true)
})
})
Loading

0 comments on commit ed67383

Please sign in to comment.