def test_all_pipette_models_can_transfer(): from opentrons.config import pipette_config models = [ 'p10_single', 'p10_multi', 'p50_single', 'p50_multi', 'p300_single', 'p300_multi', 'p1000_single' ] for m in models: robot.reset() v1 = m + '_v1' v13 = m + '_v1.3' left = instruments._create_pipette_from_config( config=pipette_config.load(v1), mount='left', name=v1) right = instruments._create_pipette_from_config( config=pipette_config.load(v13), mount='right', name=v13) left.tip_attached = True right.tip_attached = True left.aspirate().dispense() right.aspirate().dispense()
def test_execute_function_apiv2(protocol, protocol_file, monkeypatch, virtual_smoothie_env, mock_get_attached_instr): mock_get_attached_instr.return_value[types.Mount.LEFT]\ = {'config': load('p10_single_v1.5'), 'id': 'testid'} mock_get_attached_instr.return_value[types.Mount.RIGHT]\ = {'config': load('p300_single_v1.5'), 'id': 'testid2'} entries = [] def emit_runlog(entry): nonlocal entries entries.append(entry) execute.execute( protocol.filelike, 'testosaur_v2.py', emit_runlog=emit_runlog) assert [item['payload']['text'] for item in entries if item['$'] == 'before'] == [ 'Picking up tip from A1 of Opentrons 96 Tip Rack 300 µL on 1', 'Aspirating 10.0 uL from A1 of Corning 96 Well Plate 360 µL Flat on 2 at 150.0 uL/sec', # noqa(E501), 'Dispensing 10.0 uL into B1 of Corning 96 Well Plate 360 µL Flat on 2 at 300.0 uL/sec', # noqa(E501), 'Dropping tip into H12 of Opentrons 96 Tip Rack 300 µL on 1' ]
def test_override_load(config_tempdir): cdir = CONFIG['pipette_config_overrides_dir'] existing_overrides = { 'pickUpCurrent': { 'value': 1231.213 }, 'dropTipSpeed': { 'value': 121 }, 'quirks': { 'dropTipShake': True } } existing_id = 'ohoahflaseh08102qa' with (cdir / f'{existing_id}.json').open('w') as ovf: json.dump(existing_overrides, ovf) pconf = pipette_config.load('p300_multi_v1.4', existing_id) assert pconf.pick_up_current == \ existing_overrides['pickUpCurrent']['value'] assert pconf.drop_tip_speed == existing_overrides['dropTipSpeed']['value'] assert pconf.quirks == ['dropTipShake'] new_id = '0djaisoa921jas' new_pconf = pipette_config.load('p300_multi_v1.4', new_id) assert new_pconf != pconf unspecced = pipette_config.load('p300_multi_v1.4') assert unspecced == new_pconf
def test_override_save(config_tempdir): cdir = CONFIG['pipette_config_overrides_dir'] overrides = { 'pickUpCurrent': { 'value': 1231.213 }, 'dropTipSpeed': { 'value': 121 }, 'dropTipShake': { 'value': False } } new_id = 'aoa2109j09cj2a' model = 'p300_multi_v1' old_pconf = pipette_config.load('p300_multi_v1.4', new_id) assert old_pconf.quirks == ['dropTipShake'] pipette_config.save_overrides(new_id, overrides, model) assert (cdir / f'{new_id}.json').is_file() loaded = pipette_config.load_overrides(new_id) assert loaded['pickUpCurrent']['value'] == \ overrides['pickUpCurrent']['value'] assert loaded['dropTipSpeed']['value'] == \ overrides['dropTipSpeed']['value'] new_pconf = pipette_config.load('p300_multi_v1.4', new_id) assert new_pconf.quirks == []
def test_pipette_version_1_0_and_1_3_extended_travel(): models = [ 'p10_single', 'p10_multi', 'p50_single', 'p50_multi', 'p300_single', 'p300_multi', 'p1000_single' ] for m in models: robot.reset() v1 = m + '_v1' v13 = m + '_v1.3' left = instruments._create_pipette_from_config( config=pipette_config.load(v1), mount='left', name=v1) right = instruments._create_pipette_from_config( config=pipette_config.load(v13), mount='right', name=v13) # the difference between v1 and v1.3 is that the plunger's travel # distance extended, allowing greater ranges for aspirate/dispense # and blow-out. Test that all v1.3 pipette have larger travel thant v1 left_poses = left.plunger_positions left_diff = left_poses['top'] - left_poses['blow_out'] right_poses = right.plunger_positions right_diff = right_poses['top'] - right_poses['blow_out'] assert right_diff > left_diff
def get_attached_pipettes(self): """ Gets model names of attached pipettes :return: :dict with keys 'left' and 'right' and a model string for each mount, or 'uncommissioned' if no model string available """ left_data = { 'mount_axis': 'z', 'plunger_axis': 'b', 'model': self.model_by_mount['left']['model'], 'name': self.model_by_mount['left']['name'], 'id': self.model_by_mount['left']['id'] } left_model = left_data.get('model') if left_model: tip_length = pipette_config.load(left_model, left_data['id']).tip_length left_data.update({'tip_length': tip_length}) right_data = { 'mount_axis': 'a', 'plunger_axis': 'c', 'model': self.model_by_mount['right']['model'], 'name': self.model_by_mount['right']['name'], 'id': self.model_by_mount['right']['id'] } right_model = right_data.get('model') if right_model: tip_length = pipette_config.load(right_model, right_data['id']).tip_length right_data.update({'tip_length': tip_length}) return {'left': left_data, 'right': right_data}
def fake_gai(expected): return { Mount.LEFT: { 'config': pc.load(model2[0]), 'id': 'fakeid' }, Mount.RIGHT: { 'config': pc.load(model2[0]), 'id': 'fakeid2' } }
def __init__(self, model: 'PipetteModel', inst_offset_config: Dict[str, Tuple[float, float, float]], pipette_id: str = None) -> None: self._config = pipette_config.load(model, pipette_id) self._name = name_for_model(model) self._model = model self._model_offset = self._config.model_offset self._current_volume = 0.0 self._working_volume = self._config.max_volume self._current_tip_length = 0.0 self._current_tiprack_diameter = 0.0 self._fallback_tip_length = self._config.tip_length self._tip_overlap_map = self._config.tip_overlap self._has_tip = False self._pipette_id = pipette_id pip_type = 'multi' if self._config.channels == 8 else 'single' self._instrument_offset = Point(*inst_offset_config[pip_type]) self._log = mod_log.getChild( self._pipette_id if self._pipette_id else '<unknown>') self._log.info("loaded: {}, instr offset {}".format( model, self._instrument_offset)) self.ready_to_aspirate = False #: True if ready to aspirate self._aspirate_flow_rate\ = self._config.default_aspirate_flow_rates['2.0'] self._dispense_flow_rate\ = self._config.default_dispense_flow_rates['2.0'] self._blow_out_flow_rate\ = self._config.default_blow_out_flow_rates['2.0']
def test_shake_during_pick_up(monkeypatch, robot, instruments): robot.reset() pip = instruments._create_pipette_from_config( config=pipette_config.load('p1000_single_v2.0'), mount='left', name='p1000_single_v2.0') tiprack = containers_load(robot, 'opentrons_96_tiprack_1000ul', '1') shake_tips_pick_up = mock.Mock(side_effect=pip._shake_off_tips_pick_up) monkeypatch.setattr(pip, '_shake_off_tips_pick_up', shake_tips_pick_up) # Test double shake for after pick up tips pip.pick_up_tip(tiprack[0]) assert shake_tips_pick_up.call_count == 2 actual_calls = [] def mock_jog(pose_tree, axis, distance): actual_calls.append((axis, distance)) monkeypatch.setattr(pip, '_jog', mock_jog) # Test shake in both x and y shake_tips_pick_up() expected_calls = [('x', -0.3), ('x', 0.6), ('x', -0.3), ('y', -0.3), ('y', 0.6), ('y', -0.3), ('z', 20)] assert actual_calls == expected_calls pip.tip_attached = False
def __init__(self, config: TopConfigurationContext, existingInstance: Pipette): super(EnhancedPipetteV1, self).__init__(config) # load the config (again) in order to extract some more data later from opentrons.config import pipette_config from opentrons.config.pipette_config import configs pipette_model_version, pip_id = instruments._pipette_details( self.mount, self.name) self.pipette_config = pipette_config.load(pipette_model_version, pip_id) if not hasattr(self.pipette_config, 'drop_tip_min'): # future-proof cfg = configs[ pipette_model_version] # ignores the id-based overrides done by pipette_config.load, but we can live with that self.pipette_config_drop_tip_min = cfg['dropTip'][ 'min'] # hack: can't add field to pipette_config, so we do it this way else: self.pipette_config_drop_tip_min = self.pipette_config.drop_tip_min # try to mitigate effects of static electricity on small pipettes: they can cling to the tip on drop, causing disasters when next tips are picked up if self.config.enable_enhancements and self.name == 'p10_single': # dropping twice probably will help # if 'doubleDropTip' not in self.quirks: # not necessary, in the end, it seems # self.quirks.append('doubleDropTip') # plunging lower also helps, clearly self.plunger_positions[ 'drop_tip'] = self.pipette_config_drop_tip_min
async def test_get_pipettes(async_server, async_client, monkeypatch): test_model = 'p300_multi_v1' test_name = 'p300_multi' test_id = '123abc' hw = async_server['com.opentrons.hardware'] hw._backend._attached_instruments = { types.Mount.RIGHT: {'model': test_model, 'id': test_id}, types.Mount.LEFT: {'model': test_model, 'id': test_id} } model = pipette_config.load(test_model) expected = { 'left': { 'model': test_model, 'name': test_name, 'tip_length': model.tip_length, 'mount_axis': 'z', 'plunger_axis': 'b', 'id': test_id }, 'right': { 'model': test_model, 'name': test_name, 'tip_length': model.tip_length, 'mount_axis': 'a', 'plunger_axis': 'c', 'id': test_id } } resp = await async_client.get('/pipettes?refresh=true') text = await resp.text() assert resp.status == 200 assert json.loads(text) == expected
def pipette_by_name( self, mount, name_or_model, trash_container='', tip_racks=[], aspirate_flow_rate=None, dispense_flow_rate=None, min_volume=None, max_volume=None, blow_out_flow_rate=None): pipette_model_version, pip_id = self._pipette_details( mount, name_or_model) config = pipette_config.load(pipette_model_version, pip_id) if pip_id and config.backcompat_name == name_or_model: log.warning( f"Using a deprecated constructor for {pipette_model_version}") constructor_config = pipette_config.name_config()[name_or_model] config = config._replace( min_volume=constructor_config['minVolume'], max_volume=constructor_config['maxVolume']) name_or_model = config.name return self._create_pipette_from_config( config=config, mount=mount, name=name_or_model, model=pipette_model_version, trash_container=trash_container, tip_racks=tip_racks, aspirate_flow_rate=aspirate_flow_rate, dispense_flow_rate=dispense_flow_rate, min_volume=min_volume, max_volume=max_volume, blow_out_flow_rate=blow_out_flow_rate)
async def save_z(data): """ Save the current Z height value for the calibration data :param data: Information obtained from a POST request. The content type is application/json. The correct packet form should be as follows: { 'token': UUID token from current session start 'command': 'save z' } """ if not session.tip_length: message = "Tip length must be set before calibrating" status = 400 else: if not feature_flags.use_protocol_api_v2(): mount = 'Z' if session.current_mount == 'left' else 'A' actual_z = position(mount, session.adapter)[-1] length_offset = pipette_config.load( session.current_model, session.pipette_id).model_offset[-1] session.z_value = actual_z - session.tip_length + length_offset else: session.z_value = position(session.current_mount, session.adapter, session.cp)[-1] session.current_transform[2][3] = session.z_value session.adapter.update_config(gantry_calibration=list( map(lambda i: list(i), session.current_transform))) message = "Saved z: {}".format(session.z_value) status = 200 return web.json_response({'message': message}, status=status)
def mock_hw(hardware): pip = pipette.Pipette(load("p300_single_v2.1", 'testId'), { 'single': [0, 0, 0], 'multi': [0, 0, 0] }, 'testId') hardware._attached_instruments = {Mount.RIGHT: pip} hardware._current_pos = Point(0, 0, 0) async def async_mock(*args, **kwargs): pass async def async_mock_move_rel(*args, **kwargs): delta = kwargs.get('delta', Point(0, 0, 0)) hardware._current_pos += delta async def async_mock_move_to(*args, **kwargs): to_pt = kwargs.get('abs_position', Point(0, 0, 0)) hardware._current_pos = to_pt async def gantry_pos_mock(*args, **kwargs): return hardware._current_pos hardware.move_rel = MagicMock(side_effect=async_mock_move_rel) hardware.pick_up_tip = MagicMock(side_effect=async_mock) hardware.drop_tip = MagicMock(side_effect=async_mock) hardware.gantry_position = MagicMock(side_effect=gantry_pos_mock) hardware.move_to = MagicMock(side_effect=async_mock_move_to) hardware.get_instrument_max_height.return_value = 180 return hardware
def _query_mount( self, mount: Mount, expected: Union[PipetteModel, PipetteName, None]) -> AttachedInstrument: found_model: Optional[PipetteModel]\ = self._smoothie_driver.read_pipette_model( # type: ignore mount.name.lower()) if found_model and found_model not in pipette_config.config_models: # TODO: Consider how to handle this error - it bubbles up now # and will cause problems at higher levels MODULE_LOG.error(f'Bad model on {mount.name}: {found_model}') found_model = None found_id = self._smoothie_driver.read_pipette_id(mount.name.lower()) if found_model: config = pipette_config.load(found_model, found_id) if expected: acceptable = [config.name] + config.back_compat_names if expected not in acceptable: raise RuntimeError(f'mount {mount}: instrument' f' {expected} was requested' f' but {config.model} is present') return {'config': config, 'id': found_id} else: if expected: raise RuntimeError(f'mount {mount}: instrument {expected} was' f' requested, but no instrument is present') return {'config': None, 'id': None}
def test_volume_tracking(): for config in pipette_config.config_models: loaded = pipette_config.load(config) pip = pipette.Pipette(config, { 'single': [0, 0, 0], 'multi': [0, 0, 0] }, 'testID') assert pip.current_volume == 0.0 assert pip.available_volume == loaded.max_volume assert pip.ok_to_add_volume(loaded.max_volume - 0.1) pip.set_current_volume(0.1) with pytest.raises(AssertionError): pip.set_current_volume(loaded.max_volume + 0.1) with pytest.raises(AssertionError): pip.set_current_volume(-1) assert pip.current_volume == 0.1 pip.remove_current_volume(0.1) with pytest.raises(AssertionError): pip.remove_current_volume(0.1) assert pip.current_volume == 0.0 pip.set_current_volume(loaded.max_volume) assert not pip.ok_to_add_volume(0.1) with pytest.raises(AssertionError): pip.add_current_volume(0.1) assert pip.current_volume == loaded.max_volume
def test_flow_rate_setting(): pip = pipette.Pipette(pipette_config.load('p300_single_v2.0'), { 'single': [0, 0, 0], 'multi': [0, 0, 0] }, 'testId') # pipettes should load settings from config at init time assert pip.aspirate_flow_rate\ == pip.config.default_aspirate_flow_rates['2.0'] assert pip.dispense_flow_rate\ == pip.config.default_dispense_flow_rates['2.0'] assert pip.blow_out_flow_rate\ == pip.config.default_blow_out_flow_rates['2.0'] # changing flow rates with normal property access shouldn't touch # config or other flow rates config = pip.config pip.aspirate_flow_rate = 2 assert pip.aspirate_flow_rate == 2 assert pip.dispense_flow_rate\ == pip.config.default_dispense_flow_rates['2.0'] assert pip.blow_out_flow_rate\ == pip.config.default_blow_out_flow_rates['2.0'] assert pip.config is config pip.dispense_flow_rate = 3 assert pip.aspirate_flow_rate == 2 assert pip.dispense_flow_rate == 3 assert pip.blow_out_flow_rate\ == pip.config.default_blow_out_flow_rates['2.0'] assert pip.config is config pip.blow_out_flow_rate = 4 assert pip.aspirate_flow_rate == 2 assert pip.dispense_flow_rate == 3 assert pip.blow_out_flow_rate == 4 assert pip.config is config
def cache_instrument_models(self): """ Queries Smoothie for the model and ID strings of attached pipettes, and saves them so they can be reported without querying Smoothie again (as this could interrupt a command if done during a run or other movement). Shape of return dict should be: ``` { "left": { "model": "<model_string>" or None, "id": "<pipette_id_string>" or None }, "right": { "model": "<model_string>" or None, "id": "<pipette_id_string>" or None } } ``` :return: a dict with pipette data (shape described above) """ log.debug("Updating instrument model cache") for mount in self.model_by_mount.keys(): model_value = self._driver.read_pipette_model(mount) if model_value: name_value = pipette_config.name_for_model(model_value) else: name_value = None plunger_axis = 'B' if mount == 'left' else 'C' mount_axis = 'Z' if mount == 'left' else 'A' if model_value: cfg = pipette_config.load(model_value) home_pos = cfg.home_position max_travel = cfg.max_travel steps_mm = cfg.steps_per_mm else: home_pos = self.config.default_pipette_configs['homePosition'] max_travel = self.config.default_pipette_configs['maxTravel'] steps_mm = self.config.default_pipette_configs['stepsPerMM'] self._driver.update_steps_per_mm({plunger_axis: steps_mm}) self._driver.update_pipette_config(mount_axis, {'home': home_pos}) self._driver.update_pipette_config(plunger_axis, {'max_travel': max_travel}) if model_value: id_response = self._driver.read_pipette_id(mount) else: id_response = None self.model_by_mount[mount] = { 'model': model_value, 'id': id_response, 'name': name_value } log.debug("{}: {} [{}]".format(mount, self.model_by_mount[mount]['model'], self.model_by_mount[mount]['id']))
def mock_hw_pipette_all_combos(request): model = request.param return pipette.Pipette(load(model, 'testId'), { 'single': [0, 0, 0], 'multi': [0, 0, 0] }, 'testId')
def test_user_flow_select_pipette(pipettes, target_mount, hardware): pip, pip2 = None, None if pipettes[0]: pip = pipette.Pipette(load(pipettes[0], 'testId'), { 'single': [0, 0, 0], 'multi': [0, 0, 0] }, 'testId') if pipettes[1]: pip2 = pipette.Pipette(load(pipettes[1], 'testId'), { 'single': [0, 0, 0], 'multi': [0, 0, 0] }, 'testId2') hardware._attached_instruments = {Mount.LEFT: pip, Mount.RIGHT: pip2} uf = DeckCalibrationUserFlow(hardware=hardware) assert uf._hw_pipette == \ hardware._attached_instruments[target_mount]
def fake_attached(stuff): return { mount: { 'config': pc.load(value['model']), 'id': value['id'] } for mount, value in dummy_instruments.items() }
def test_tip_overlap(config_model): loaded = pipette_config.load(config_model) pip = pipette.Pipette(loaded, { 'single': [0, 0, 0], 'multi': [0, 0, 0] }, 'testId') assert pip.config.tip_overlap\ == pipette_config.configs[config_model]['tipOverlap']
def _attached_to_mount( self, mount: types.Mount, expected_instr: Optional[PipetteName]) -> AttachedInstrument: init_instr = self._attached_instruments.get(mount, { 'model': None, 'id': None }) found_model = init_instr['model'] back_compat: List['PipetteName'] = [] if found_model: back_compat = configs[found_model].get('backCompatNames', []) if expected_instr and found_model\ and (not found_model.startswith(expected_instr) and expected_instr not in back_compat): if self._strict_attached: raise RuntimeError( 'mount {}: expected instrument {} but got {}'.format( mount.name, expected_instr, found_model)) else: return { 'config': load(dummy_model_for_name(expected_instr)), 'id': None } elif found_model and expected_instr: # Instrument detected matches instrument expected (note: # "instrument detected" means passed as an argument to the # constructor of this class) return { 'config': load(found_model, init_instr['id']), 'id': init_instr['id'] } elif found_model: # Instrument detected and no expected instrument specified return { 'config': load(found_model, init_instr['id']), 'id': init_instr['id'] } elif expected_instr: # Expected instrument specified and no instrument detected return { 'config': load(dummy_model_for_name(expected_instr)), 'id': None } else: # No instrument detected or expected return {'config': None, 'id': None}
def test_config_update(config_model): loaded = pipette_config.load(config_model) pip = pipette.Pipette(loaded, { 'single': [0, 0, 0], 'multi': [0, 0, 0] }, 'testID') sample_plunger_pos = {'top': 19.5} pip.update_config_item('top', sample_plunger_pos.get('top')) assert pip.config.top == sample_plunger_pos.get('top')
def test_shake_during_drop(monkeypatch): robot.reset() pip = instruments._create_pipette_from_config( config=pipette_config.load('p1000_single_v2.0'), mount='left', name='p1000_single_v2.0') tiprack = containers_load(robot, 'opentrons_96_tiprack_1000ul', '1') shake_tips_drop = mock.Mock( side_effect=pip._shake_off_tips_drop) monkeypatch.setattr(pip, '_shake_off_tips_drop', shake_tips_drop) # Test single shake for after pick up tips pip.tip_attached = True pip.drop_tip(tiprack.wells(0)) assert shake_tips_drop.call_count == 1 actual_calls = [] def jog_side_effect(pose_tree, axis, distance): actual_calls.append((axis, distance)) jog = mock.Mock(side_effect=jog_side_effect) monkeypatch.setattr(pip, '_jog', jog) # Test shake only in x, with no location passed, shake distance is 2.25 shake_tips_drop() expected_calls = [('x', -2.25), ('x', 4.5), ('x', -2.25), ('z', 20)] assert actual_calls == expected_calls # Test drop tip shake at a well with diameter above upper limit (2.25 mm) tiprack.wells(0).properties['width'] = 2.3*4 actual_calls.clear() shake_tips_drop(tiprack.wells(0)) expected_calls = [('x', -2.25), ('x', 4.5), ('x', -2.25), ('z', 20)] assert actual_calls == expected_calls # Test drop tip shake at a well with diameter between upper limit # and lower limit (1.00 - 2.25 mm) tiprack.wells(0).properties['width'] = 2*4 actual_calls.clear() shake_tips_drop(tiprack.wells(0)) expected_calls = [('x', -2), ('x', 4), ('x', -2), ('z', 20)] assert actual_calls == expected_calls # Test drop tip shake at a well with diameter below lower limit (1.00 mm) tiprack.wells(0).properties['width'] = 0.9*4 actual_calls.clear() shake_tips_drop(tiprack.wells(0)) expected_calls = [('x', -1), ('x', 2), ('x', -1), ('z', 20)] assert actual_calls == expected_calls pip.tip_attached = False
def test_versioned_aspiration(pipette_model, monkeypatch): monkeypatch.setattr(ff, 'use_old_aspiration_functions', lambda: True) was = pipette_config.load(pipette_model) check_sequences_close( was.ul_per_mm['aspirate'], defs['config'][pipette_model]['ulPerMm'][0]['aspirate']) check_sequences_close( was.ul_per_mm['dispense'], defs['config'][pipette_model]['ulPerMm'][0]['dispense']) monkeypatch.setattr(ff, 'use_old_aspiration_functions', lambda: False) now = pipette_config.load(pipette_model) check_sequences_close( now.ul_per_mm['aspirate'], defs['config'][pipette_model]['ulPerMm'][-1]['aspirate']) check_sequences_close( now.ul_per_mm['dispense'], defs['config'][pipette_model]['ulPerMm'][-1]['dispense']) assert now.ul_per_mm['aspirate'] != was.ul_per_mm['aspirate']
def test_pipette_models_reach_max_volume(robot, instruments): for model in pipette_config.config_models: config = pipette_config.load(model) robot.reset() pipette = instruments._create_pipette_from_config(config=config, mount='right', name=model) pipette.tip_attached = True pipette.aspirate(pipette.max_volume) pos = pose_tracker.absolute(robot.poses, pipette.instrument_actuator) assert pos[0] < pipette.plunger_positions['top']
def test_execute_extra_labware(protocol, protocol_file, monkeypatch, virtual_smoothie_env, mock_get_attached_instr): fixturedir = HERE / '..' / '..' / '..' /\ 'shared-data' / 'labware' / 'fixtures' / '2' entries = [] def emit_runlog(entry): nonlocal entries entries.append(entry) mock_get_attached_instr.return_value[types.Mount.RIGHT] = { 'config': load('p300_single_v2.0'), 'id': 'testid'} # make sure we can load labware explicitly # make sure we don't have an exception from not finding the labware execute.execute(protocol.filelike, 'custom_labware.py', emit_runlog=emit_runlog, custom_labware_paths=[str(fixturedir)]) # instead of 4 in simulate because we get before and after assert len(entries) == 8 protocol.filelike.seek(0) # make sure we don't get autoload behavior when not on a robot with pytest.raises(ExceptionInProtocolError, match='.*FileNotFoundError.*'): execute.execute(protocol.filelike, 'custom_labware.py') no_lw = execute.get_protocol_api('2.0') assert not no_lw._extra_labware protocol.filelike.seek(0) monkeypatch.setattr(execute, 'IS_ROBOT', True) monkeypatch.setattr(execute, 'JUPYTER_NOTEBOOK_LABWARE_DIR', fixturedir) # make sure we don't have an exception from not finding the labware entries = [] execute.execute(protocol.filelike, 'custom_labware.py', emit_runlog=emit_runlog) # instead of 4 in simulate because we get before and after assert len(entries) == 8 # make sure the extra labware loaded by default is right ctx = execute.get_protocol_api('2.0') assert len(ctx._extra_labware.keys()) == len(os.listdir(fixturedir)) assert ctx.load_labware('fixture_12_trough', 1, namespace='fixture') # if there is no labware dir, make sure everything still works monkeypatch.setattr(execute, 'JUPYTER_NOTEBOOK_LABWARE_DIR', HERE / 'nosuchdirectory') ctx = execute.get_protocol_api('2.0') with pytest.raises(FileNotFoundError): ctx.load_labware("fixture_12_trough", 1, namespace='fixture')
def test_ul_per_mm_continuous(pipette_model): """ For each model of pipette, for each boundary between pieces of the piecewise function describing the ul/mm relationship, test that the function is continuous. This test is utilizing the intermediate value theorem to determine whether a value c lives in the bounds of [a, b]. In this case, we are checking that given volumes (X) in a range of lower middle and max, the output (Y) of the func lives within the range of lower and max. See here for further details: https://en.wikipedia.org/wiki/Intermediate_value_theorem """ config = pipette_config.load(pipette_model) aspirate = config.ul_per_mm['aspirate'] dispense = config.ul_per_mm['dispense'] min_vol = 0.000001 # sufficiently small starting volume for lno in range(len(aspirate) - 1): line = aspirate[lno] curr_max_vol = line[0] # find a halfway point roughly between max and min volume for a given # piecewise sequence of a pipette function half_max_vol = (curr_max_vol - min_vol) / 2 + min_vol min_ul_per_mm = line[1] * min_vol + line[2] mid_ul_per_mm = line[1] * half_max_vol + line[2] max_ul_per_mm = line[1] * curr_max_vol + line[2] lower_mm = min_ul_per_mm / min_vol higher_mm = max_ul_per_mm / curr_max_vol half_mm = mid_ul_per_mm / half_max_vol range_1 = (half_mm >= lower_mm) and (half_mm <= higher_mm) range_2 = (half_mm <= lower_mm) and (half_mm >= higher_mm) assert range_1 or range_2 min_vol = curr_max_vol # make sure the mm of movement for max aspirate and max dispense agree aspirate_seq = aspirate[len(aspirate) - 1] dispense_seq = dispense[len(dispense) - 1] pip_max_vol = config.max_volume aspirate_mm = (aspirate_seq[1] * pip_max_vol + aspirate_seq[2]) / pip_max_vol dispense_mm = (dispense_seq[1] * pip_max_vol + dispense_seq[2]) / pip_max_vol # for many of the older pipettes, the aspirate and dispense values are # not the same. assert isclose(round(aspirate_mm), round(dispense_mm))
def test_tip_tracking(): pip = pipette.Pipette(pipette_config.load('p10_single_v1'), { 'single': [0, 0, 0], 'multi': [0, 0, 0] }, 'testID') with pytest.raises(AssertionError): pip.remove_tip() assert not pip.has_tip tip_length = 25.0 pip.add_tip(tip_length) assert pip.has_tip with pytest.raises(AssertionError): pip.add_tip(tip_length) pip.remove_tip() assert not pip.has_tip with pytest.raises(AssertionError): pip.remove_tip()