Skip to content

Commit

Permalink
fix(robot-server): save custom tiprack def on robot during tip length…
Browse files Browse the repository at this point in the history
… cal (#7231)

* fix(robot-server): save custom tiprack def on robot during tip length cal

* remove log statements

* add missing module

* fix lint error

* add tests
  • Loading branch information
ahiuchingau authored Jan 20, 2021
1 parent 1e180c8 commit 4fe2b37
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 15 deletions.
20 changes: 20 additions & 0 deletions api/src/opentrons/calibration_storage/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
labware calibration from its designated file location.
"""
import itertools
import json
import typing
from typing_extensions import Literal

Expand Down Expand Up @@ -282,3 +283,22 @@ def get_all_pipette_offset_calibrations() \
source=_get_calibration_source(data),
status=_get_calibration_status(data)))
return all_calibrations


def get_custom_tiprack_definition_for_tlc(labware_uri: str) -> 'LabwareDefinition':
"""
Return the custom tiprack definition saved in the custom tiprack directory
during tip length calibration
"""
custom_tiprack_dir = config.get_custom_tiprack_def_path()
custom_tiprack_path = custom_tiprack_dir / f'{labware_uri}.json'
try:
with open(custom_tiprack_path, 'rb') as f:
return json.loads(f.read().decode('utf-8'))
except FileNotFoundError:
raise FileNotFoundError(
f'Custom tiprack {labware_uri} not found in the custom tiprack'
'directory on the robot. Please recalibrate tip length and '
'pipette offset with this tiprack before performing calibration '
'health check.'
)
15 changes: 15 additions & 0 deletions api/src/opentrons/calibration_storage/modify.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from opentrons import config
from opentrons.types import Mount, Point

from opentrons.protocols.api_support.constants import OPENTRONS_NAMESPACE
from opentrons.util.helpers import utc_now

from . import (
Expand Down Expand Up @@ -140,10 +141,24 @@ def create_tip_length_data(
'uri': labware_uri
}

if not definition.get('namespace') == OPENTRONS_NAMESPACE:
_save_custom_tiprack_definition(labware_uri, definition)

data = {labware_hash + parent: tip_length_data}
return data


def _save_custom_tiprack_definition(
labware_uri: str, definition: 'LabwareDefinition'):
namespace, load_name, version = labware_uri.split('/')
custom_tr_dir_path = config.get_custom_tiprack_def_path()
custom_namespace_dir = custom_tr_dir_path/f'{namespace}/{load_name}'
custom_namespace_dir.mkdir(parents=True, exist_ok=True)

custom_tr_def_path = custom_namespace_dir/f'{version}.json'
io.save_to_file(custom_tr_def_path, definition)


def _helper_offset_data_format(filepath: str, delta: Point) -> dict:
if not Path(filepath).is_file():
calibration_data = {
Expand Down
12 changes: 11 additions & 1 deletion api/src/opentrons/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,13 @@ class ConfigElement(NamedTuple):
'Pipette Calibration Directory',
Path('robot') / 'pipettes',
ConfigElementType.DIR,
'The dir where pipette calibration is stored')
'The dir where pipette calibration is stored'),
ConfigElement('custom_tiprack_dir',
'Custom Tiprack Directory',
Path('tip_lengths') / 'custom_tiprack_definitions',
ConfigElementType.DIR,
'The dir where custom tiprack definitions for tip length '
'calibration are stored')
)
#: The available configuration file elements to modify. All of these can be
#: changed by editing opentrons.json, where the keys are the name elements,
Expand Down Expand Up @@ -506,3 +512,7 @@ def get_opentrons_path(path_name: str) -> Path:

def get_tip_length_cal_path():
return get_opentrons_path('tip_length_calibration_dir')


def get_custom_tiprack_def_path():
return get_opentrons_path('custom_tiprack_dir')
36 changes: 29 additions & 7 deletions api/tests/opentrons/protocol_api/test_offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

MOCK_HASH = 'mock_hash'
PIPETTE_ID = 'pipette_id'
URI = 'custom/minimal_labware_def/1'

minimalLabwareDef = {
"metadata": {
Expand Down Expand Up @@ -62,7 +63,7 @@
"yDimension": 2.0,
"zDimension": 3.0
},
"namespace": "opentrons",
"namespace": "custom",
"version": 1
}

Expand All @@ -78,6 +79,11 @@ def tlc_path(pip_id):
/ '{}.json'.format(pip_id)


def custom_tiprack_path(uri):
return config.get_custom_tiprack_def_path() \
/ '{}.json'.format(uri)


def mock_hash_labware(labware_def):
return MOCK_HASH

Expand All @@ -95,6 +101,19 @@ def clear_calibration(monkeypatch):
pass


@pytest.fixture
def clear_custom_tiprack_dir(monkeypatch):
try:
os.remove(custom_tiprack_path(URI))
except FileNotFoundError:
pass
yield
try:
os.remove(custom_tiprack_path(URI))
except FileNotFoundError:
pass


@pytest.fixture
def clear_tlc_calibration(monkeypatch):
try:
Expand Down Expand Up @@ -139,7 +158,7 @@ def test_json_datetime_encoder():
assert decoded == original


def test_create_tip_length_calibration_data(monkeypatch):
def test_create_tip_length_calibration_data(monkeypatch, clear_custom_tiprack_dir):

fake_time = utc_now()

Expand All @@ -157,15 +176,18 @@ def test_create_tip_length_calibration_data(monkeypatch):
'lastModified': fake_time,
'source': cs_types.SourceType.user,
'status': {'markedBad': False},
'uri': 'opentrons/minimal_labware_def/1'
'uri': URI
}
}
assert not os.path.exists(custom_tiprack_path(URI))
result = modify.create_tip_length_data(
minimalLabwareDef, parent, tip_length)
assert result == expected_data
assert os.path.exists(custom_tiprack_path(URI))


def test_save_tip_length_calibration_data(monkeypatch, clear_tlc_calibration):
def test_save_tip_length_calibration_data(monkeypatch,
clear_tlc_calibration):
assert not os.path.exists(tlc_path(PIPETTE_ID))

test_data = {
Expand Down Expand Up @@ -239,7 +261,7 @@ def test_load_tip_length_calibration_data(monkeypatch, clear_tlc_calibration):
source=cs_types.SourceType.user,
status=cs_types.CalibrationStatus(markedBad=False),
tiprack=MOCK_HASH,
uri='opentrons/minimal_labware_def/1',
uri='custom/minimal_labware_def/1',
last_modified=test_data[MOCK_HASH]['lastModified']
)
assert result == expected
Expand All @@ -256,9 +278,9 @@ def test_clear_tip_length_calibration_data(monkeypatch):
}
json.dump(test_offset, offset_file)

assert len(os.listdir(calpath)) > 0
assert len([f for f in os.listdir(calpath) if f.endswith('.json')]) > 0
delete.clear_tip_length_calibration()
assert len(os.listdir(calpath)) == 0
assert len([f for f in os.listdir(calpath) if f.endswith('.json')]) == 0


def test_schema_shape(monkeypatch, clear_calibration):
Expand Down
22 changes: 17 additions & 5 deletions robot-server/robot_server/robot/calibration/check/user_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from opentrons.hardware_control import (
ThreadManager, CriticalPoint, Pipette, robot_calibration, util)
from opentrons.protocol_api import labware
from opentrons.protocols.api_support.constants import OPENTRONS_NAMESPACE
from opentrons.config import feature_flags as ff
from opentrons.protocols.geometry.deck import Deck

Expand Down Expand Up @@ -311,13 +312,18 @@ def _get_tip_length_from_pipette(
return None
details = helpers.details_from_uri(pip_offset.uri)
position = self._deck.position_for(TIPRACK_SLOT)
tiprack = labware.load(load_name=details.load_name,
namespace=details.namespace,
version=details.version,
parent=position)
if details.namespace == OPENTRONS_NAMESPACE:
tiprack = labware.load(load_name=details.load_name,
namespace=details.namespace,
version=details.version,
parent=position)
tiprack_def = tiprack._implementation.get_definition()
else:
tiprack_def = get.get_custom_tiprack_definition_for_tlc(
pip_offset.uri)
return get.load_tip_length_calibration(
pipette.pipette_id,
tiprack._implementation.get_definition(),
tiprack_def,
'')

def _check_valid_calibrations(self):
Expand Down Expand Up @@ -402,6 +408,12 @@ def _get_tr_lw(tip_rack_def: Optional['LabwareDefinition'],
try:
details \
= helpers.details_from_uri(existing_calibration.uri)
if not details.namespace == OPENTRONS_NAMESPACE:
tiprack_def = get.get_custom_tiprack_definition_for_tlc(
existing_calibration.uri)
return labware.load_from_definition(
definition=tiprack_def,
parent=position)
return labware.load(load_name=details.load_name,
namespace=details.namespace,
version=details.version,
Expand Down
63 changes: 63 additions & 0 deletions robot-server/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,66 @@ def _get_labware_fixture(fixture_name):
return json.loads(f.read().decode('utf-8'))

return _get_labware_fixture


@pytest.fixture
def custom_tiprack_def():
return {
"metadata": {
"displayName": "minimal labware"
},
"cornerOffsetFromSlot": {
"x": 10,
"y": 10,
"z": 5
},
"parameters": {
"isTiprack": True,
"tipLength": 55.3,
"tipOverlap": 2.8,
"loadName": "minimal_labware_def"
},
"ordering": [["A1"], ["A2"]],
"wells": {
"A1": {
"depth": 40,
"totalLiquidVolume": 100,
"diameter": 30,
"x": 0,
"y": 0,
"z": 0,
"shape": "circular"
},
"A2": {
"depth": 40,
"totalLiquidVolume": 100,
"diameter": 30,
"x": 10,
"y": 0,
"z": 0,
"shape": "circular"
}
},
"dimensions": {
"xDimension": 1.0,
"yDimension": 2.0,
"zDimension": 3.0
},
"namespace": "custom",
"version": 1
}


@pytest.fixture
def clear_custom_tiprack_def_dir():
tiprack_path = config.get_custom_tiprack_def_path() \
/ 'custom/minimal_labware_def/1.json'
try:
os.remove(tiprack_path)
except FileNotFoundError:
pass
yield
try:
os.remove(tiprack_path)
except FileNotFoundError:
pass
47 changes: 46 additions & 1 deletion robot-server/tests/robot/calibration/check/test_user_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest
from opentrons.hardware_control import pipette
from opentrons.types import Mount, Point
from opentrons.calibration_storage import get, types as CSTypes
from opentrons.calibration_storage import get, modify, types as CSTypes
from opentrons.config import robot_configs
from opentrons.config.pipette_config import load

Expand Down Expand Up @@ -140,6 +140,14 @@ def build_mock_stored_pipette_offset(kind='normal'):
uri='opentrons/opentrons_96_filtertiprack_200ul/1',
source=CSTypes.SourceType.user,
status=CSTypes.CalibrationStatus(markedBad=False)))
elif kind == 'custom_tiprack':
return MagicMock(
return_value=CSTypes.PipetteOffsetByPipetteMount(
offset=[0, 1, 2],
tiprack='tiprack-id',
uri='custom/minimal_labware_def/1',
source=CSTypes.SourceType.user,
status=CSTypes.CalibrationStatus(markedBad=False)))
else:
return MagicMock(return_value=None)

Expand All @@ -155,6 +163,16 @@ def build_mock_stored_tip_length(kind='normal'):
status=CSTypes.CalibrationStatus(markedBad=False),
uri='path/to_my_labware/1')
return MagicMock(return_value=tip_length)
elif kind == 'custom_tiprack':
tip_length = CSTypes.TipLengthCalibration(
tip_length=30,
pipette='fake id',
tiprack='fake_hash',
last_modified='some time',
source=CSTypes.SourceType.user,
status=CSTypes.CalibrationStatus(markedBad=False),
uri='custom/minimal_labware_def/1')
return MagicMock(return_value=tip_length)
else:
return MagicMock(return_value=None)

Expand Down Expand Up @@ -201,6 +219,33 @@ def test_load_labware(mock_hw):
assert len(uf.get_required_labware()) == 2


def test_load_custom_tiprack(mock_hw,
custom_tiprack_def,
clear_custom_tiprack_def_dir):
# save custom tiprack definition to custom tiprack directory
modify._save_custom_tiprack_definition(
'custom/minimal_labware_def/1',
custom_tiprack_def
)
# load a labware with calibrations
with patch.object(
get,
'get_robot_deck_attitude',
new=build_mock_deck_calibration()),\
patch.object(
get,
'load_tip_length_calibration',
new=build_mock_stored_tip_length('custom_tiprack')),\
patch.object(
get, 'get_pipette_offset',
new=build_mock_stored_pipette_offset('custom_tiprack')):
uf = CheckCalibrationUserFlow(
hardware=mock_hw, has_calibration_block=True)
assert uf.active_tiprack._implementation.get_display_name() ==\
'minimal labware on 8'
assert len(uf.get_required_labware()) == 2


def test_bad_calibration(mock_hw):
with pytest.raises(RobotServerError):
CheckCalibrationUserFlow(hardware=mock_hw)
Expand Down
Loading

0 comments on commit 4fe2b37

Please sign in to comment.