diff --git a/api/opentrons/__init__.py b/api/opentrons/__init__.py index a372a1f38fb..4a3f196986f 100755 --- a/api/opentrons/__init__.py +++ b/api/opentrons/__init__.py @@ -66,7 +66,9 @@ def P10_Single( aspirate_flow_rate=None, dispense_flow_rate=None): - config = pipette_config.load('p10_single_v1') + pipette_model_version = self._retrieve_version_number( + mount, 'p10_single') + config = pipette_config.load(pipette_model_version) return self._create_pipette_from_config( config=config, @@ -84,7 +86,9 @@ def P10_Multi( aspirate_flow_rate=None, dispense_flow_rate=None): - config = pipette_config.load('p10_multi_v1') + pipette_model_version = self._retrieve_version_number( + mount, 'p10_multi') + config = pipette_config.load(pipette_model_version) return self._create_pipette_from_config( config=config, @@ -102,7 +106,9 @@ def P50_Single( aspirate_flow_rate=None, dispense_flow_rate=None): - config = pipette_config.load('p50_single_v1') + pipette_model_version = self._retrieve_version_number( + mount, 'p50_single') + config = pipette_config.load(pipette_model_version) return self._create_pipette_from_config( config=config, @@ -120,7 +126,9 @@ def P50_Multi( aspirate_flow_rate=None, dispense_flow_rate=None): - config = pipette_config.load('p50_multi_v1') + pipette_model_version = self._retrieve_version_number( + mount, 'p50_multi') + config = pipette_config.load(pipette_model_version) return self._create_pipette_from_config( config=config, @@ -138,7 +146,9 @@ def P300_Single( aspirate_flow_rate=None, dispense_flow_rate=None): - config = pipette_config.load('p300_single_v1') + pipette_model_version = self._retrieve_version_number( + mount, 'p300_single') + config = pipette_config.load(pipette_model_version) return self._create_pipette_from_config( config=config, @@ -156,7 +166,9 @@ def P300_Multi( aspirate_flow_rate=None, dispense_flow_rate=None): - config = pipette_config.load('p300_multi_v1') + pipette_model_version = self._retrieve_version_number( + mount, 'p300_multi') + config = pipette_config.load(pipette_model_version) return self._create_pipette_from_config( config=config, @@ -174,7 +186,9 @@ def P1000_Single( aspirate_flow_rate=None, dispense_flow_rate=None): - config = pipette_config.load('p1000_single_v1') + pipette_model_version = self._retrieve_version_number( + mount, 'p1000_single') + config = pipette_config.load(pipette_model_version) return self._create_pipette_from_config( config=config, @@ -219,6 +233,20 @@ def _create_pipette_from_config( p.set_pick_up_current(config.pick_up_current) return p + def _retrieve_version_number(self, mount, expected_model_substring): + # pass a default pipette model-version, for when robot is simulating + # this allows any pipette to be simulated, regardless of what is + # actually attached/cached on the robot's mounts + default_model = expected_model_substring + '_v1' # default to v1 + if robot.is_simulating(): + return default_model + + attached_model = robot.get_attached_pipettes()[mount]['model'] + if attached_model and expected_model_substring in attached_model: + return attached_model + else: + return default_model + instruments = InstrumentsWrapper(robot) containers = ContainersWrapper(robot) diff --git a/api/opentrons/api/session.py b/api/opentrons/api/session.py index ff58e406476..99e08f58bf3 100755 --- a/api/opentrons/api/session.py +++ b/api/opentrons/api/session.py @@ -119,13 +119,16 @@ def on_command(message): try: # TODO (artyom, 20171005): this will go away # once robot / driver simulation flow is fixed - robot._driver.disconnect() + robot.disconnect() if self._is_json_protocol: execute_protocol(self._protocol) else: exec(self._protocol, {}) finally: - robot._driver.connect() + # physically attached pipettes are re-cached during robot.connect() + # which is important, because during a simulation, the robot could + # think that it holds a pipette model that it actually does not + robot.connect() unsubscribe() # Accumulate containers, instruments, interactions from commands diff --git a/api/opentrons/drivers/smoothie_drivers/driver_3_0.py b/api/opentrons/drivers/smoothie_drivers/driver_3_0.py index 3f793609b4b..91195fccfcf 100755 --- a/api/opentrons/drivers/smoothie_drivers/driver_3_0.py +++ b/api/opentrons/drivers/smoothie_drivers/driver_3_0.py @@ -380,6 +380,11 @@ def read_pipette_model(self, mount): # Backward compatibility for pipettes programmed with model # strings that did not include the _v# designation res = res + '_v1' + elif res and '_v13' in res: + # Backward compatibility for pipettes programmed with model + # strings that did not include the "." to seperate version + # major and minor values + res = res.replace('_v13', 'v1.3') return res diff --git a/api/opentrons/instruments/pipette.py b/api/opentrons/instruments/pipette.py index 3f797956db0..c551cb56e06 100755 --- a/api/opentrons/instruments/pipette.py +++ b/api/opentrons/instruments/pipette.py @@ -1385,9 +1385,7 @@ def _aspirate_plunger_position(self, ul): ul_per_mm = lambda: self.ul_per_mm else: ul_per_mm = self._key_map_pipette_functions(model, ul, 'aspirate') - millimeters = 0.0 - if ul_per_mm is not None: - millimeters = ul / ul_per_mm() + millimeters = ul / ul_per_mm() destination_mm = self._get_plunger_position('bottom') + millimeters return round(destination_mm, 6) @@ -1407,24 +1405,29 @@ def _dispense_plunger_position(self, ul): else: ul_per_mm = self._key_map_pipette_functions(model, ul, 'dispense') # Change default of 1000 ul/mm if function returns a value - millimeters = 0.0 - if ul_per_mm is not None: - # If ul_per_mm not None, it is a function so must make it callable - millimeters = ul / ul_per_mm() + millimeters = ul / ul_per_mm() destination_mm = self._get_plunger_position('bottom') + millimeters return round(destination_mm, 6) def _key_map_pipette_functions(self, model, ul, func): function_map = { 'p10_single_v1': lambda: self._p10_single_piecewise(ul, func), + 'p10_single_v1.3': lambda: self._p10_single_piecewise(ul, func), 'p10_multi_v1': lambda: self._p10_multi_piecewise(ul, func), + 'p10_multi_v1.3': lambda: self._p10_multi_piecewise(ul, func), 'p50_single_v1': lambda: self._p50_single_piecewise(ul, func), + 'p50_single_v1.3': lambda: self._p50_single_piecewise(ul, func), 'p50_multi_v1': lambda: self._p50_multi_piecewise(ul, func), + 'p50_multi_v1.3': lambda: self._p50_multi_piecewise(ul, func), 'p300_single_v1': lambda: self._p300_single_piecewise(ul, func), + 'p300_single_v1.3': lambda: self._p300_single_piecewise(ul, func), 'p300_multi_v1': lambda: self._p300_multi_piecewise(ul, func), - 'p1000_single_v1': lambda: self._p1000_piecewise(ul, func)} + 'p300_multi_v1.3': lambda: self._p300_multi_piecewise(ul, func), + 'p1000_single_v1': lambda: self._p1000_piecewise(ul, func), + 'p1000_single_v1.3': lambda: self._p1000_piecewise(ul, func) + } - return function_map.get(model) + return function_map[model] def _p10_single_piecewise(self, ul, func): # Piecewise function that calculates ul_per_mm for a p10 single @@ -1514,11 +1517,10 @@ def _p300_multi_piecewise(self, ul, func): return 0*ul + 19.29389273 def _p1000_piecewise(self, ul, func): - if ul > 0: - if func == 'aspirate': - return 65 - else: - return 65 + if func == 'aspirate': + return 65 + else: + return 65 def _volume_percentage(self, volume): """Returns the plunger percentage for a given volume. @@ -1741,10 +1743,8 @@ def set_flow_rate(self, aspirate=None, dispense=None): else: ul_per_mm = self._key_map_pipette_functions( model, ul, 'aspirate') - - if ul_per_mm is not None: - self.set_speed( - aspirate=round(aspirate / ul_per_mm(), 6)) + self.set_speed( + aspirate=round(aspirate / ul_per_mm(), 6)) if dispense: # Set the ul_per_mm for the pipette if self.ul_per_mm: @@ -1752,9 +1752,8 @@ def set_flow_rate(self, aspirate=None, dispense=None): else: ul_per_mm = self._key_map_pipette_functions( model, ul, 'dispense') - if ul_per_mm is not None: - self.set_speed( - dispense=round(dispense / ul_per_mm(), 6)) + self.set_speed( + dispense=round(dispense / ul_per_mm(), 6)) return self def set_pick_up_current(self, amperes): diff --git a/api/opentrons/instruments/pipette_config.py b/api/opentrons/instruments/pipette_config.py index 6abd3289fa2..fd18887a407 100644 --- a/api/opentrons/instruments/pipette_config.py +++ b/api/opentrons/instruments/pipette_config.py @@ -94,7 +94,7 @@ def _load_config_dict_from_file(pipette_model: str) -> dict: if os.path.exists(config_file): with open(config_file) as conf: all_configs = json.load(conf) - cfg = all_configs[pipette_model] + cfg = all_configs.get(pipette_model, cfg) return cfg @@ -129,12 +129,12 @@ def _load_config_dict_from_file(pipette_model: str) -> dict: # TODO because this is the backup in case that behavior fails, # TODO but we could make it more reliable if we start bundling # TODO config data into the wheel file perhaps. Needs research. -p10_single = pipette_config( +p10_single_v1 = pipette_config( plunger_positions={ - 'top': 19, - 'bottom': 2.5, - 'blow_out': -0.5, - 'drop_tip': -4 + 'top': 19.5, + 'bottom': 2, + 'blow_out': -1, + 'drop_tip': -4.5 }, pick_up_current=0.1, aspirate_flow_rate=10 / DEFAULT_ASPIRATE_SECONDS, @@ -148,12 +148,31 @@ def _load_config_dict_from_file(pipette_model: str) -> dict: tip_length=33 ) -p10_multi = pipette_config( +p10_single_v1_3 = pipette_config( plunger_positions={ - 'top': 19, - 'bottom': 4, - 'blow_out': 1, - 'drop_tip': -4.5 + 'top': 19.5, + 'bottom': 0.5, + 'blow_out': -2.5, + 'drop_tip': -6 + }, + pick_up_current=0.1, + aspirate_flow_rate=10 / DEFAULT_ASPIRATE_SECONDS, + dispense_flow_rate=10 / DEFAULT_DISPENSE_SECONDS, + channels=1, + name='p10_single_v1.3', + model_offset=[0.0, 0.0, Z_OFFSET_P10], + plunger_current=0.3, + drop_tip_current=0.5, + max_volume=10, + tip_length=33 +) + +p10_multi_v1 = pipette_config( + plunger_positions={ + 'top': 19.5, + 'bottom': 2, + 'blow_out': -1, + 'drop_tip': -4 }, pick_up_current=0.2, aspirate_flow_rate=10 / DEFAULT_ASPIRATE_SECONDS, @@ -167,12 +186,31 @@ def _load_config_dict_from_file(pipette_model: str) -> dict: tip_length=33 ) -p50_single = pipette_config( +p10_multi_v1_3 = pipette_config( plunger_positions={ - 'top': 19, - 'bottom': 2.5, + 'top': 19.5, + 'bottom': 0.5, + 'blow_out': -2.5, + 'drop_tip': -5.5 + }, + pick_up_current=0.2, + aspirate_flow_rate=10 / DEFAULT_ASPIRATE_SECONDS, + dispense_flow_rate=10 / DEFAULT_DISPENSE_SECONDS, + channels=8, + name='p10_multi_v1.3', + model_offset=[0.0, Y_OFFSET_MULTI, Z_OFFSET_MULTI], + plunger_current=0.5, + drop_tip_current=0.5, + max_volume=10, + tip_length=33 +) + +p50_single_v1 = pipette_config( + plunger_positions={ + 'top': 19.5, + 'bottom': 2.01, 'blow_out': 2, - 'drop_tip': -5 + 'drop_tip': -4.5 }, pick_up_current=0.1, aspirate_flow_rate=50 / DEFAULT_ASPIRATE_SECONDS, @@ -186,12 +224,31 @@ def _load_config_dict_from_file(pipette_model: str) -> dict: tip_length=51.7 ) -p50_multi = pipette_config( +p50_single_v1_3 = pipette_config( plunger_positions={ - 'top': 19, + 'top': 19.5, + 'bottom': 2, + 'blow_out': 0.5, + 'drop_tip': -6 + }, + pick_up_current=0.1, + aspirate_flow_rate=50 / DEFAULT_ASPIRATE_SECONDS, + dispense_flow_rate=50 / DEFAULT_DISPENSE_SECONDS, + channels=1, + name='p50_single_v1.3', + model_offset=[0.0, 0.0, Z_OFFSET_P50], + plunger_current=0.3, + drop_tip_current=0.5, + max_volume=50, + tip_length=51.7 +) + +p50_multi_v1 = pipette_config( + plunger_positions={ + 'top': 19.5, 'bottom': 2.5, 'blow_out': 2, - 'drop_tip': -4 + 'drop_tip': -3.5 }, pick_up_current=0.3, aspirate_flow_rate=50 / DEFAULT_ASPIRATE_SECONDS, @@ -205,13 +262,32 @@ def _load_config_dict_from_file(pipette_model: str) -> dict: tip_length=51.7 ) -p300_single = pipette_config( +p50_multi_v1_3 = pipette_config( plunger_positions={ - 'top': 19, - 'bottom': 2.5, - 'blow_out': 1, + 'top': 19.5, + 'bottom': 2, + 'blow_out': 0.5, 'drop_tip': -5 }, + pick_up_current=0.3, + aspirate_flow_rate=50 / DEFAULT_ASPIRATE_SECONDS, + dispense_flow_rate=50 / DEFAULT_DISPENSE_SECONDS, + channels=8, + name='p50_multi_v1.3', + model_offset=[0.0, Y_OFFSET_MULTI, Z_OFFSET_MULTI], + plunger_current=0.5, + drop_tip_current=0.5, + max_volume=50, + tip_length=51.7 +) + +p300_single_v1 = pipette_config( + plunger_positions={ + 'top': 19.5, + 'bottom': 1.5, + 'blow_out': 0, + 'drop_tip': -4 + }, pick_up_current=0.1, aspirate_flow_rate=300 / DEFAULT_ASPIRATE_SECONDS, dispense_flow_rate=300 / DEFAULT_DISPENSE_SECONDS, @@ -224,12 +300,31 @@ def _load_config_dict_from_file(pipette_model: str) -> dict: tip_length=51.7 ) -p300_multi = pipette_config( +p300_single_v1_3 = pipette_config( plunger_positions={ - 'top': 19, - 'bottom': 3, - 'blow_out': 1, - 'drop_tip': -3.5 + 'top': 19.5, + 'bottom': 1.5, + 'blow_out': -1.5, + 'drop_tip': -5.5 + }, + pick_up_current=0.1, + aspirate_flow_rate=300 / DEFAULT_ASPIRATE_SECONDS, + dispense_flow_rate=300 / DEFAULT_DISPENSE_SECONDS, + channels=1, + name='p300_single_v1.3', + model_offset=[0.0, 0.0, Z_OFFSET_P300], + plunger_current=0.3, + drop_tip_current=0.5, + max_volume=300, + tip_length=51.7 +) + +p300_multi_v1 = pipette_config( + plunger_positions={ + 'top': 19.5, + 'bottom': 3.5, + 'blow_out': 3, + 'drop_tip': -2 }, pick_up_current=0.3, aspirate_flow_rate=300 / DEFAULT_ASPIRATE_SECONDS, @@ -243,9 +338,28 @@ def _load_config_dict_from_file(pipette_model: str) -> dict: tip_length=51.7 ) -p1000_single = pipette_config( +p300_multi_v1_3 = pipette_config( + plunger_positions={ + 'top': 19.5, + 'bottom': 3.5, + 'blow_out': 1.5, + 'drop_tip': -3.5 + }, + pick_up_current=0.3, + aspirate_flow_rate=300 / DEFAULT_ASPIRATE_SECONDS, + dispense_flow_rate=300 / DEFAULT_DISPENSE_SECONDS, + channels=8, + name='p300_multi_v1.3', + model_offset=[0.0, Y_OFFSET_MULTI, Z_OFFSET_MULTI], + plunger_current=0.5, + drop_tip_current=0.5, + max_volume=300, + tip_length=51.7 +) + +p1000_single_v1 = pipette_config( plunger_positions={ - 'top': 19, + 'top': 19.5, 'bottom': 3, 'blow_out': 1, 'drop_tip': -5 @@ -262,14 +376,40 @@ def _load_config_dict_from_file(pipette_model: str) -> dict: tip_length=76.7 ) +p1000_single_v1_3 = pipette_config( + plunger_positions={ + 'top': 19.5, + 'bottom': 2.5, + 'blow_out': -0.5, + 'drop_tip': -4 + }, + pick_up_current=0.1, + aspirate_flow_rate=1000 / DEFAULT_ASPIRATE_SECONDS, + dispense_flow_rate=1000 / DEFAULT_DISPENSE_SECONDS, + channels=1, + name='p1000_single_v1.3', + model_offset=[0.0, 0.0, Z_OFFSET_P1000], + plunger_current=0.5, + drop_tip_current=0.5, + max_volume=1000, + tip_length=76.7 +) + fallback_configs = { - 'p10_single_v1': p10_single, - 'p10_multi_v1': p10_multi, - 'p50_single_v1': p50_single, - 'p50_multi_v1': p50_multi, - 'p300_single_v1': p300_single, - 'p300_multi_v1': p300_multi, - 'p1000_single_v1': p1000_single + 'p10_single_v1': p10_single_v1, + 'p10_single_v1.3': p10_single_v1_3, + 'p10_multi_v1': p10_multi_v1, + 'p10_multi_v1.3': p10_multi_v1_3, + 'p50_single_v1': p50_single_v1, + 'p50_single_v1.3': p50_single_v1_3, + 'p50_multi_v1': p50_multi_v1, + 'p50_multi_v1.3': p50_multi_v1_3, + 'p300_single_v1': p300_single_v1, + 'p300_single_v1.3': p300_single_v1_3, + 'p300_multi_v1': p300_multi_v1, + 'p300_multi_v1.3': p300_multi_v1_3, + 'p1000_single_v1': p1000_single_v1, + 'p1000_single_v1.3': p1000_single_v1_3, } @@ -292,33 +432,9 @@ def select_config(model: str): # protocol writer -# model-specific ID's, saved with each Pipette's memory -# used to identifiy what model pipette is currently connected to machine -PIPETTE_MODEL_IDENTIFIERS = { - 'single': { - '10': 'p10_single_v1', - '50': 'p50_single_v1', - '300': 'p300_single_v1', - '1000': 'p1000_single_v1' - }, - 'multi': { - '10': 'p10_multi_v1', - '50': 'p50_multi_v1', - '300': 'p300_multi_v1', - } -} - - configs = { model: select_config(model) - for model in [ - 'p10_single_v1', - 'p10_multi_v1', - 'p50_single_v1', - 'p50_multi_v1', - 'p300_single_v1', - 'p300_multi_v1', - 'p1000_single_v1']} + for model in fallback_configs.keys()} def load(pipette_model: str) -> pipette_config: diff --git a/api/opentrons/robot/robot.py b/api/opentrons/robot/robot.py index cb9530f8989..fdc48338fde 100755 --- a/api/opentrons/robot/robot.py +++ b/api/opentrons/robot/robot.py @@ -501,6 +501,11 @@ def connect(self, port=None, options=None): for module in self.modules: module.connect() self.fw_version = self._driver.get_fw_version() + + # the below call to `cache_instrument_models` is relied upon by + # `Session._simulate()`, which calls `robot.connect()` after exec'ing a + # protocol. That protocol could very well have different pipettes than + # what are physically attached to the robot self.cache_instrument_models() def _update_axis_homed(self, *args): diff --git a/api/opentrons/tools/write_pipette_memory.py b/api/opentrons/tools/write_pipette_memory.py index 8da5db733e7..8421c83271f 100644 --- a/api/opentrons/tools/write_pipette_memory.py +++ b/api/opentrons/tools/write_pipette_memory.py @@ -11,14 +11,14 @@ 'P300M': 'p300_multi_v1', 'P1000S': 'p1000_single_v1' }, - 'v13': { - 'P10SV13': 'p10_single_v13', - 'P10MV13': 'p10_multi_v13', - 'P50SV13': 'p50_single_v13', - 'P50MV13': 'p50_multi_v13', - 'P3HSV13': 'p300_single_v13', - 'P3HMV13': 'p300_multi_v13', - 'P1KSV13': 'p1000_single_v13' + 'v1.3': { + 'P10SV13': 'p10_single_v1.3', + 'P10MV13': 'p10_multi_v1.3', + 'P50SV13': 'p50_single_v1.3', + 'P50MV13': 'p50_multi_v1.3', + 'P3HSV13': 'p300_single_v1.3', + 'P3HMV13': 'p300_multi_v1.3', + 'P1KSV13': 'p1000_single_v1.3' } } @@ -42,7 +42,6 @@ def write_identifiers(robot, mount, new_id, new_model): robot._driver.write_pipette_id(mount, new_id) read_id = robot._driver.read_pipette_id(mount) _assert_the_same(new_id, read_id['pipette_id']) - robot._driver.write_pipette_model(mount, new_model) read_model = robot._driver.read_pipette_model(mount) _assert_the_same(new_model, read_model) @@ -50,10 +49,7 @@ def write_identifiers(robot, mount, new_id, new_model): def check_previous_data(robot, mount): old_id = robot._driver.read_pipette_id(mount) - if old_id.get('pipette_id'): - old_id = old_id.get('pipette_id') - else: - old_id = None + old_id = old_id.get('pipette_id') old_model = robot._driver.read_pipette_model(mount) if old_id and old_model: print( @@ -79,13 +75,14 @@ def _user_submitted_barcode(max_length): # remove all characters before the letter P # for example, remove ASCII selector code "\x1b(B" on chinese keyboards barcode = barcode[barcode.index('P'):] + barcode = barcode.split('\r')[0].split('\n')[0] # remove any newlines return barcode def _parse_model_from_barcode(barcode): - # MUST iterate through v13 first, because v1 barcodes did not have + # MUST iterate through v1.3 first, because v1 barcodes did not have # characters to specify the version number - for version in ['v13', 'v1']: + for version in ['v1.3', 'v1']: for barcode_substring in MODELS[version].keys(): if barcode.startswith(barcode_substring): return MODELS[version][barcode_substring] diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index dfbd0ef1f80..a535e8ba2c5 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -325,7 +325,7 @@ def model(robot): from opentrons.containers import load from opentrons.instruments.pipette import Pipette - pipette = Pipette(robot, mount='right') + pipette = Pipette(robot, ul_per_mm=18.5, mount='right') plate = load(robot, '96-flat', '1') instrument = models.Instrument(pipette) diff --git a/api/tests/opentrons/containers/test_grid.py b/api/tests/opentrons/containers/test_grid.py index a603d434fbc..d00795c562b 100644 --- a/api/tests/opentrons/containers/test_grid.py +++ b/api/tests/opentrons/containers/test_grid.py @@ -69,6 +69,7 @@ def test_serial_dilution(self): p200 = pipette.Pipette( self.robot, + ul_per_mm=18.5, trash_container=trash, tip_racks=[tiprack], min_volume=10, diff --git a/api/tests/opentrons/labware/test_pipette.py b/api/tests/opentrons/labware/test_pipette.py index aec7f561332..ae9522f6e2c 100755 --- a/api/tests/opentrons/labware/test_pipette.py +++ b/api/tests/opentrons/labware/test_pipette.py @@ -7,61 +7,76 @@ from numpy import isclose -def test_pipette_models(): - # Test that the configuration position values for a given pipette model - # still make sense with any changes to the piecewise functions - robot.reset() - p = instruments.P10_Single(mount='left') - ul_per_mm = Pipette._p10_single_piecewise(p, 10, 'aspirate') - p.max_volume = calculate_max_volume(p, ul_per_mm) - assert p.channels == 1 - assert p.max_volume > 10 - p = instruments.P10_Multi(mount='right') - ul_per_mm = Pipette._p10_multi_piecewise(p, 10, 'aspirate') - p.max_volume = calculate_max_volume(p, ul_per_mm) - assert p.channels == 8 - assert p.max_volume > 10 - - robot.reset() - p = instruments.P50_Single(mount='left') - ul_per_mm = 3.1347 # Change once config blow_out position adjusted - p.max_volume = calculate_max_volume(p, ul_per_mm) - assert p.channels == 1 - assert p.max_volume > 50 - p = instruments.P50_Multi(mount='right') - ul_per_mm = Pipette._p50_multi_piecewise(p, 50, 'aspirate') - p.max_volume = calculate_max_volume(p, ul_per_mm) - assert p.channels == 8 - assert p.max_volume > 50 - - robot.reset() - p = instruments.P300_Single(mount='left') - ul_per_mm = Pipette._p300_single_piecewise(p, 300, 'aspirate') - p.max_volume = calculate_max_volume(p, ul_per_mm) - assert p.channels == 1 - assert p.max_volume > 300 - p = instruments.P300_Multi(mount='right') - ul_per_mm = Pipette._p300_multi_piecewise(p, 300, 'aspirate') - p.max_volume = calculate_max_volume(p, ul_per_mm) - assert p.channels == 8 - assert p.max_volume > 300 - - robot.reset() - p = instruments.P1000_Single(mount='left') - ul_per_mm = Pipette._p1000_piecewise(p, 1000, 'aspirate') - p.max_volume = calculate_max_volume(p, ul_per_mm) - assert p.channels == 1 - assert p.max_volume > 1000 - - -def calculate_max_volume(pipette, ul_per_mm): - t = pipette._get_plunger_position('top') - b = pipette._get_plunger_position('bottom') - return (t - b) * ul_per_mm +def test_pipette_version_1_0_and_1_3_extended_travel(): + from opentrons.instruments import pipette_config + + models = [ + 'p10_single', 'p10_multi', 'p50_single', 'p50_multi', + 'p300_single', 'p300_multi', 'p1000_single' + ] + + for m in models: + robot.reset() + left = instruments._create_pipette_from_config( + config=pipette_config.load(m + '_v1'), + mount='left') + right = instruments._create_pipette_from_config( + config=pipette_config.load(m + '_v1.3'), + mount='right') + + # 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 test_all_pipette_models_can_transfer(): + from opentrons.instruments import pipette_config + + models = [ + 'p10_single', 'p10_multi', 'p50_single', 'p50_multi', + 'p300_single', 'p300_multi', 'p1000_single' + ] + + for m in models: + robot.reset() + left = instruments._create_pipette_from_config( + config=pipette_config.load(m + '_v1'), + mount='left') + right = instruments._create_pipette_from_config( + config=pipette_config.load(m + '_v1.3'), + mount='right') + + left.tip_attached = True + right.tip_attached = True + left.aspirate().dispense() + right.aspirate().dispense() + + +def test_pipette_models_reach_max_volume(): + from opentrons.instruments import pipette_config + + for model, config in pipette_config.configs.items(): + robot.reset() + pipette = instruments._create_pipette_from_config( + config=config, + mount='right') + + 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_set_flow_rate(): # Test new flow-rate functionality on all pipettes with different max vols + robot.reset() p10 = instruments.P10_Single(mount='right') p10.set_flow_rate(aspirate=10) @@ -170,7 +185,7 @@ def test_aspirate_move_to(): robot.poses, p300.instrument_actuator) - assert (current_pos == (7.889964, 0.0, 0.0)).all() + assert (current_pos == (6.889964, 0.0, 0.0)).all() current_pos = pose_tracker.absolute(robot.poses, p300) assert isclose(current_pos, (175.34, 127.94, 10.5)).all() @@ -198,7 +213,7 @@ def test_dispense_move_to(): current_pos = pose_tracker.absolute( robot.poses, p300.instrument_actuator) - assert (current_pos == (2.5, 0.0, 0.0)).all() + assert (current_pos == (1.5, 0.0, 0.0)).all() current_pos = pose_tracker.absolute(robot.poses, p300) assert isclose(current_pos, (175.34, 127.94, 10.5)).all() diff --git a/api/tests/opentrons/labware/test_pipette_unittest.py b/api/tests/opentrons/labware/test_pipette_unittest.py index 52d4f2137de..6e27b50045e 100644 --- a/api/tests/opentrons/labware/test_pipette_unittest.py +++ b/api/tests/opentrons/labware/test_pipette_unittest.py @@ -23,6 +23,7 @@ def setUp(self): self.p200 = Pipette( self.robot, + ul_per_mm=18.5, trash_container=self.trash, tip_racks=[self.tiprack1, self.tiprack2], min_volume=10, # These are variable @@ -45,7 +46,7 @@ def test_bad_volume_percentage(self): def test_add_instrument(self): self.robot.reset() - Pipette(self.robot, mount='left') + Pipette(self.robot, ul_per_mm=18.5, mount='left') self.assertRaises(RuntimeError, Pipette, self.robot, mount='left') def test_aspirate_zero_volume(self): @@ -85,6 +86,7 @@ def test_deprecated_axis_call(self): def test_get_instruments_by_name(self): self.p1000 = Pipette( self.robot, + ul_per_mm=18.5, trash_container=self.trash, tip_racks=[self.tiprack1], min_volume=10, # These are variable @@ -212,7 +214,7 @@ def test_distribute(self): ['Aspirating', '70', 'well A1'], ['Dispensing', '30', 'well H1'], ['Dispensing', '30', 'well A2'], - ['Blow', 'well A1'], + ['Blow', 'well A1'] ] fuzzy_assert(self.robot.commands(), expected=expected) self.robot.clear_commands() @@ -681,9 +683,7 @@ def test_distribute_air_gap(self): ['blow', 'Well A1'], ['drop'] ] - fuzzy_assert(self.robot.commands(), - expected=expected - ) + fuzzy_assert(self.robot.commands(), expected=expected) self.robot.clear_commands() def test_distribute_air_gap_and_disposal_vol(self): @@ -978,8 +978,6 @@ def test_mix_with_named_args(self): self.p200.dispense = mock.Mock() self.p200.mix(volume=50, repetitions=2) - print(self.p200.tip_racks) - self.assertEqual( self.p200.dispense.mock_calls, [ @@ -1047,7 +1045,8 @@ def test_tip_tracking_chain(self): mount='right', tip_racks=[self.tiprack1, self.tiprack2], trash_container=self.tiprack1, - name='pipette-for-transfer-tests' + name='pipette-for-transfer-tests', + ul_per_mm=18.5 ) self.p200.max_volume = 200 @@ -1088,7 +1087,8 @@ def test_tip_tracking_chain_multi_channel(self): tip_racks=[self.tiprack1, self.tiprack2], min_volume=10, # These are variable mount='right', - channels=8 + channels=8, + ul_per_mm=18.5 ) p200_multi.calibrate_plunger( @@ -1159,8 +1159,6 @@ def test_direct_movement_within_well(self): mock.call( self.plate[2].bottom(), instrument=self.p200, strategy='direct') ] - from pprint import pprint - pprint(self.robot.move_to.mock_calls) self.assertEqual(self.robot.move_to.mock_calls, expected) def build_pick_up_tip(self, well): diff --git a/api/tests/opentrons/tools/test_qc_scripts.py b/api/tests/opentrons/tools/test_qc_scripts.py index 44e2e3632bf..62c14ff3842 100644 --- a/api/tests/opentrons/tools/test_qc_scripts.py +++ b/api/tests/opentrons/tools/test_qc_scripts.py @@ -8,13 +8,13 @@ 'P300S20180101A01': 'p300_single_v1', 'P300M20180101A01': 'p300_multi_v1', 'P1000S20180101A01': 'p1000_single_v1', - 'P10SV1318010101': 'p10_single_v13', - 'P10MV1318010102': 'p10_multi_v13', - 'P50SV1318010103': 'p50_single_v13', - 'P50MV1318010104': 'p50_multi_v13', - 'P3HSV1318010105': 'p300_single_v13', - 'P3HMV1318010106': 'p300_multi_v13', - 'P1KSV1318010107': 'p1000_single_v13' + 'P10SV1318010101': 'p10_single_v1.3', + 'P10MV1318010102': 'p10_multi_v1.3', + 'P50SV1318010103': 'p50_single_v1.3', + 'P50MV1318010104': 'p50_multi_v1.3', + 'P3HSV1318010105': 'p300_single_v1.3', + 'P3HMV1318010106': 'p300_multi_v1.3', + 'P1KSV1318010107': 'p1000_single_v1.3' } diff --git a/shared-data/robot-data/pipette-config.json b/shared-data/robot-data/pipette-config.json index ad317461159..72681ffd35c 100644 --- a/shared-data/robot-data/pipette-config.json +++ b/shared-data/robot-data/pipette-config.json @@ -3,121 +3,235 @@ "displayName": "P10 Single-Channel", "nominalMaxVolumeUl": 10, "plungerPositions": { - "top": 19, - "bottom": 2.5, - "blowOut": -0.5, - "dropTip": -4 + "top": 19.5, + "bottom": 2, + "blowOut": -1, + "dropTip": -4.5 }, "pickUpCurrent": 0.1, "aspirateFlowRate": 5, "dispenseFlowRate": 10, - "ulPerMm": 0.77, "channels": 1, "modelOffset": [0.0, 0.0, -13], "plungerCurrent": 0.3, "dropTipCurrent": 0.5, + "maxVolume": 10, + "tipLength": 33 + }, + "p10_single_v1.3": { + "displayName": "P10 Single-Channel", + "nominalMaxVolumeUl": 10, + "plungerPositions": { + "top": 19.5, + "bottom": 0.5, + "blowOut": -2.5, + "dropTip": -6 + }, + "pickUpCurrent": 0.1, + "aspirateFlowRate": 5, + "dispenseFlowRate": 10, + "channels": 1, + "modelOffset": [0.0, 0.0, -13], + "plungerCurrent": 0.3, + "dropTipCurrent": 0.5, + "maxVolume": 10, "tipLength": 33 }, "p10_multi_v1": { "displayName": "P10 8-Channel", "nominalMaxVolumeUl": 10, "plungerPositions": { - "top": 19, - "bottom": 4, - "blowOut": 1, - "dropTip": -4.5 + "top": 19.5, + "bottom": 2, + "blowOut": -1, + "dropTip": -4 }, "pickUpCurrent": 0.2, "aspirateFlowRate": 5, "dispenseFlowRate": 10, - "ulPerMm": 0.77, "channels": 8, "modelOffset": [0.0, 31.5, -25.8], "plungerCurrent": 0.5, "dropTipCurrent": 0.5, + "maxVolume": 10, + "tipLength": 33 + }, + "p10_multi_v1.3": { + "displayName": "P10 8-Channel", + "nominalMaxVolumeUl": 10, + "plungerPositions": { + "top": 19.5, + "bottom": 0.5, + "blowOut": -2.5, + "dropTip": -5.5 + }, + "pickUpCurrent": 0.2, + "aspirateFlowRate": 5, + "dispenseFlowRate": 10, + "channels": 8, + "modelOffset": [0.0, 31.5, -25.8], + "plungerCurrent": 0.5, + "dropTipCurrent": 0.5, + "maxVolume": 10, "tipLength": 33 }, "p50_single_v1": { "displayName": "P50 Single-Channel", "nominalMaxVolumeUl": 50, "plungerPositions": { - "top": 19, - "bottom": 2.5, + "top": 19.5, + "bottom": 2.01, "blowOut": 2, - "dropTip": -5 + "dropTip": -4.5 + }, + "pickUpCurrent": 0.1, + "aspirateFlowRate": 25, + "dispenseFlowRate": 50, + "channels": 1, + "modelOffset": [0.0, 0.0, 0.0], + "plungerCurrent": 0.3, + "dropTipCurrent": 0.5, + "maxVolume": 50, + "tipLength": 51.7 + }, + "p50_single_v1.3": { + "displayName": "P50 Single-Channel", + "nominalMaxVolumeUl": 50, + "plungerPositions": { + "top": 19.5, + "bottom": 2, + "blowOut": 0.5, + "dropTip": -6 }, "pickUpCurrent": 0.1, "aspirateFlowRate": 25, "dispenseFlowRate": 50, - "ulPerMm": 3.35, "channels": 1, "modelOffset": [0.0, 0.0, 0.0], "plungerCurrent": 0.3, "dropTipCurrent": 0.5, + "maxVolume": 50, "tipLength": 51.7 }, "p50_multi_v1": { "displayName": "P50 8-Channel", "nominalMaxVolumeUl": 50, "plungerPositions": { - "top": 19, + "top": 19.5, "bottom": 2.5, "blowOut": 2, - "dropTip": -4 + "dropTip": -3.5 }, "pickUpCurrent": 0.3, "aspirateFlowRate": 25, "dispenseFlowRate": 50, - "ulPerMm": 3.35, "channels": 8, "modelOffset": [0.0, 31.5, -25.8], "plungerCurrent": 0.5, "dropTipCurrent": 0.5, + "maxVolume": 50, + "tipLength": 51.7 + }, + "p50_multi_v1.3": { + "displayName": "P50 8-Channel", + "nominalMaxVolumeUl": 50, + "plungerPositions": { + "top": 19.5, + "bottom": 2, + "blowOut": 0.5, + "dropTip": -5 + }, + "pickUpCurrent": 0.3, + "aspirateFlowRate": 25, + "dispenseFlowRate": 50, + "channels": 8, + "modelOffset": [0.0, 31.5, -25.8], + "plungerCurrent": 0.5, + "dropTipCurrent": 0.5, + "maxVolume": 50, "tipLength": 51.7 }, "p300_single_v1": { "displayName": "P300 Single-Channel", "nominalMaxVolumeUl": 300, "plungerPositions": { - "top": 19, - "bottom": 2.5, - "blowOut": 1, - "dropTip": -5 + "top": 19.5, + "bottom": 1.5, + "blowOut": 0, + "dropTip": -4 + }, + "pickUpCurrent": 0.1, + "aspirateFlowRate": 150, + "dispenseFlowRate": 300, + "channels": 1, + "modelOffset": [0.0, 0.0, 0.0], + "plungerCurrent": 0.3, + "dropTipCurrent": 0.5, + "maxVolume": 300, + "tipLength": 51.7 + }, + "p300_single_v1.3": { + "displayName": "P300 Single-Channel", + "nominalMaxVolumeUl": 300, + "plungerPositions": { + "top": 19.5, + "bottom": 1.5, + "blowOut": -1.5, + "dropTip": -5.5 }, "pickUpCurrent": 0.1, "aspirateFlowRate": 150, "dispenseFlowRate": 300, - "ulPerMm": 18.7, "channels": 1, "modelOffset": [0.0, 0.0, 0.0], "plungerCurrent": 0.3, "dropTipCurrent": 0.5, + "maxVolume": 300, "tipLength": 51.7 }, "p300_multi_v1": { "displayName": "P300 8-Channel", "nominalMaxVolumeUl": 300, "plungerPositions": { - "top": 19, - "bottom": 3, - "blowOut": 1, + "top": 19.5, + "bottom": 3.5, + "blowOut": 3, + "dropTip": -2 + }, + "pickUpCurrent": 0.3, + "aspirateFlowRate": 150, + "dispenseFlowRate": 300, + "channels": 8, + "modelOffset": [0.0, 31.5, -25.8], + "plungerCurrent": 0.5, + "dropTipCurrent": 0.5, + "maxVolume": 300, + "tipLength": 51.7 + }, + "p300_multi_v1.3": { + "displayName": "P300 8-Channel", + "nominalMaxVolumeUl": 300, + "plungerPositions": { + "top": 19.5, + "bottom": 3.5, + "blowOut": 1.5, "dropTip": -3.5 }, "pickUpCurrent": 0.3, "aspirateFlowRate": 150, "dispenseFlowRate": 300, - "ulPerMm": 19, "channels": 8, "modelOffset": [0.0, 31.5, -25.8], "plungerCurrent": 0.5, "dropTipCurrent": 0.5, + "maxVolume": 300, "tipLength": 51.7 }, "p1000_single_v1": { "displayName": "P1000 Single-channel", "nominalMaxVolumeUl": 1000, "plungerPositions": { - "top": 19, + "top": 19.5, "bottom": 3, "blowOut": 1, "dropTip": -5 @@ -125,11 +239,30 @@ "pickUpCurrent": 0.1, "aspirateFlowRate": 500, "dispenseFlowRate": 1000, - "ulPerMm": 65, "channels": 1, "modelOffset": [0.0, 0.0, 20.0], "plungerCurrent": 0.5, "dropTipCurrent": 0.5, + "maxVolume": 1000, + "tipLength": 76.7 + }, + "p1000_single_v1.3": { + "displayName": "P1000 Single-channel", + "nominalMaxVolumeUl": 1000, + "plungerPositions": { + "top": 19.5, + "bottom": 2.5, + "blowOut": -0.5, + "dropTip": -4 + }, + "pickUpCurrent": 0.1, + "aspirateFlowRate": 500, + "dispenseFlowRate": 1000, + "channels": 1, + "modelOffset": [0.0, 0.0, 20.0], + "plungerCurrent": 0.5, + "dropTipCurrent": 0.5, + "maxVolume": 1000, "tipLength": 76.7 } }