def test_arc_lower_minimum_z_height(): deck = Deck() lw1 = labware.load(labware_name, deck.position_for(1)) tall_z = 100 minimum_z_height = 42 old_top = lw1.wells()[0].top() tall_point = old_top.point._replace(z=tall_z) tall_top = old_top._replace(point=tall_point) to_tall = plan_moves(lw1.wells()[2].top(), tall_top, deck, P300M_GEN2_MAX_HEIGHT, 7.0, 15.0, False, minimum_z_height=minimum_z_height) check_arc_basic(to_tall, lw1.wells()[2].top(), tall_top) assert to_tall[0][0].z == tall_z from_tall = plan_moves(tall_top, lw1.wells()[3].top(), deck, P300M_GEN2_MAX_HEIGHT, 7.0, 15.0, minimum_z_height=minimum_z_height) check_arc_basic(from_tall, tall_top, lw1.wells()[3].top()) assert from_tall[0][0].z == tall_z no_well = tall_top._replace(labware=lw1) from_tall_lw = plan_moves(no_well, lw1.wells()[4].bottom(), deck, P300M_GEN2_MAX_HEIGHT, 7.0, 15.0) check_arc_basic(from_tall_lw, no_well, lw1.wells()[4].bottom())
def set_up_index_file_temporary_directory(server_temp_directory): delete.clear_calibrations() deck = Deck() labware_list = [ 'nest_96_wellplate_2ml_deep', 'corning_384_wellplate_112ul_flat', 'geb_96_tiprack_1000ul', 'nest_12_reservoir_15ml', 'opentrons_96_tiprack_10ul'] for idx, name in enumerate(labware_list): parent = deck.position_for(idx+1) definition = labware.get_labware_definition(name) lw = labware.Labware(definition, parent) labware.save_calibration(lw, Point(0, 0, 0))
def test_direct_movs(): deck = Deck() lw1 = labware.load(labware_name, deck.position_for(1)) same_place = plan_moves(lw1.wells()[0].top(), lw1.wells()[0].top(), deck, instr_max_height=P300M_GEN2_MAX_HEIGHT) assert same_place == [(lw1.wells()[0].top().point, None)] same_well = plan_moves(lw1.wells()[0].top(), lw1.wells()[0].bottom(), deck, instr_max_height=P300M_GEN2_MAX_HEIGHT) assert same_well == [(lw1.wells()[0].bottom().point, None)]
def test_direct_minimum_z_height(): deck = Deck() lw1 = labware.load(labware_name, deck.position_for(1)) from_loc = lw1.wells()[0].bottom().move(Point(x=-2)) to_loc = lw1.wells()[0].bottom().move(Point(x=2)) zmo = 150 # This would normally be a direct move since it’s inside the same well, # but we want to check that we override it into an arc moves = plan_moves(from_loc, to_loc, deck, P300M_GEN2_MAX_HEIGHT, minimum_z_height=zmo) assert len(moves) == 3 assert moves[0][0].z == zmo # equals zmo b/c 150 is max of all safe z's check_arc_basic(moves, from_loc, to_loc)
def test_build_edges(): lw_def = get_labware_definition('corning_96_wellplate_360ul_flat') test_lw = Labware(lw_def, Location(Point(0, 0, 0), None)) off = Point(0, 0, 1.0) deck = Deck() old_correct_edges = [ test_lw['A1']._from_center_cartesian(x=1.0, y=0, z=1) + off, test_lw['A1']._from_center_cartesian(x=-1.0, y=0, z=1) + off, test_lw['A1']._from_center_cartesian(x=0, y=1.0, z=1) + off, test_lw['A1']._from_center_cartesian(x=0, y=-1.0, z=1) + off, ] res = build_edges( test_lw['A1'], 1.0, Mount.RIGHT, deck, version=APIVersion(2, 2)) assert res == old_correct_edges new_correct_edges = [ test_lw['A1']._from_center_cartesian(x=1.0, y=0, z=1) + off, test_lw['A1']._from_center_cartesian(x=-1.0, y=0, z=1) + off, test_lw['A1']._from_center_cartesian(x=0, y=0, z=1) + off, test_lw['A1']._from_center_cartesian(x=0, y=1.0, z=1) + off, test_lw['A1']._from_center_cartesian(x=0, y=-1.0, z=1) + off, ] res2 = build_edges( test_lw['A1'], 1.0, Mount.RIGHT, deck, version=APIVersion(2, 4)) assert res2 == new_correct_edges
def test_arc_tall_point(): deck = Deck() lw1 = labware.load(labware_name, deck.position_for(1)) tall_z = 100 old_top = lw1.wells()[0].top() tall_point = old_top.point._replace(z=tall_z) tall_top = old_top._replace(point=tall_point) to_tall = plan_moves(lw1.wells()[2].top(), tall_top, deck, 7.0, 15.0) check_arc_basic(to_tall, lw1.wells()[2].top(), tall_top) assert to_tall[0][0].z == tall_z from_tall = plan_moves(tall_top, lw1.wells()[3].top(), deck, 7.0, 15.0) check_arc_basic(from_tall, tall_top, lw1.wells()[3].top()) assert from_tall[0][0].z == tall_z no_well = tall_top._replace(labware=lw1) from_tall_lw = plan_moves(no_well, lw1.wells()[4].bottom(), deck, 7.0, 15.0) check_arc_basic(from_tall_lw, no_well, lw1.wells()[4].bottom())
def test_first_parent(): deck = Deck() trough = labware.load(trough_name, deck.position_for(1)) assert first_parent(trough) == '1' assert first_parent(trough['A2']) == '1' assert first_parent(None) is None assert first_parent('6') == '6' mod = module_geometry.load_module( module_geometry.TemperatureModuleModel.TEMPERATURE_V2, deck.position_for('6'), MAX_SUPPORTED_VERSION) mod_trough = mod.add_labware(labware.load(trough_name, mod.location)) assert first_parent(mod_trough['A5']) == '6' assert first_parent(mod_trough) == '6' assert first_parent(mod) == '6' mod_trough._parent = mod_trough with pytest.raises(RuntimeError): # make sure we catch cycles first_parent(mod_trough)
def test_instr_max_height(): deck = Deck() fixed_trash = deck.get_fixed_trash() trough = labware.load(trough_name, deck.position_for(1)) trough2 = labware.load(trough_name, deck.position_for(2)) deck[1] = trough deck[2] = trough2 # if the highest deck height is between 1 mm and 10 mm below # the max instrument achievable height, we use the max instrument # height as the safe height instr_max_height = fixed_trash.wells()[0].top().point.z + 1 height = safe_height(from_loc=trough.wells()[0].top(), to_loc=trough2.wells()[0].top(), deck=deck, instr_max_height=round(instr_max_height, 2), well_z_margin=7.0, lw_z_margin=15.0) assert height == round(instr_max_height, 2) # if the highest deck height is > 10 mm below the max instrument # height, we use the lw_z_margin instead instr_max_height = fixed_trash.wells()[0].top().point.z + 30 height2 = safe_height(from_loc=trough.wells()[0].top(), to_loc=trough2.wells()[0].top(), deck=deck, instr_max_height=round(instr_max_height, 2), well_z_margin=7.0, lw_z_margin=15.0) assert height2 ==\ round(fixed_trash.wells()[0].top().point.z, 2) + 15.0 # it fails if the highest deck height is less than 1 mm below # the max instr achievable height instr_max_height = fixed_trash.wells()[0].top().point.z with pytest.raises(Exception): safe_height(from_loc=trough.wells()[0].top(), to_loc=trough2.wells()[0].top(), deck=deck, instr_max_height=round(instr_max_height, 2), well_z_margin=7.0, lw_z_margin=15.0)
def test_labware_in_next_slot(): deck = Deck() trough = labware.load(trough_name, deck.position_for(4)) trough2 = labware.load(trough_name, deck.position_for(1)) trough3 = labware.load(trough_name, deck.position_for(3)) deck[4] = trough deck[1] = trough2 deck[3] = trough3 assert deck.right_of('3') is None assert deck.left_of('2') is trough2 assert deck.right_of('2') is trough3 assert deck.right_of('9') is None
def test_slot_collisions(): d = Deck() mod_slot = '7' mod = module_geometry.load_module( module_geometry.ThermocyclerModuleModel.THERMOCYCLER_V1, d.position_for(mod_slot)) d[mod_slot] = mod with pytest.raises(ValueError): d['7'] = 'not this time boyo' with pytest.raises(ValueError): d['8'] = 'nor this time boyo' with pytest.raises(ValueError): d['10'] = 'or even this time boyo' with pytest.raises(ValueError): d['11'] = 'def not this time though' lw_slot = '4' lw = labware.load(labware_name, d.position_for(lw_slot)) d[lw_slot] = lw assert lw_slot in d
def test_direct_cp(): deck = Deck() trough = labware.load(trough_name, deck.position_for(1)) lw1 = labware.load(labware_name, deck.position_for(2)) # when moving from no origin location to a centered labware we should # start in default cp from_nothing = plan_moves(Location(Point(50, 50, 50), None), trough.wells()[0].top(), deck, P300M_GEN2_MAX_HEIGHT) check_arc_basic(from_nothing, Location(Point(50, 50, 50), None), trough.wells()[0].top()) assert from_nothing[0][1] is None assert from_nothing[1][1] == CriticalPoint.XY_CENTER assert from_nothing[2][1] == CriticalPoint.XY_CENTER # when moving from an origin with a centered labware to a dest with a # centered labware we should stay in centered the entire time, whether # arc from_centered_arc = plan_moves(trough.wells()[0].top(), trough.wells()[1].top(), deck, P300M_GEN2_MAX_HEIGHT) check_arc_basic(from_centered_arc, trough.wells()[0].top(), trough.wells()[1].top()) assert from_centered_arc[0][1] == CriticalPoint.XY_CENTER assert from_centered_arc[1][1] == CriticalPoint.XY_CENTER assert from_centered_arc[2][1] == CriticalPoint.XY_CENTER # or direct from_centered_direct = plan_moves(trough.wells()[0].top(), trough.wells()[1].bottom(), deck, P300M_GEN2_MAX_HEIGHT) assert from_centered_direct[0][1] == CriticalPoint.XY_CENTER # when moving from centered to normal, only the first move should be # centered to_normal = plan_moves(trough.wells()[0].top(), lw1.wells()[0].top(), deck, P300M_GEN2_MAX_HEIGHT) check_arc_basic(to_normal, trough.wells()[0].top(), lw1.wells()[0].top()) assert to_normal[0][1] == CriticalPoint.XY_CENTER assert to_normal[1][1] is None assert to_normal[2][1] is None
def test_no_labware_loc(labware_offset_tempdir): labware_def = labware.get_labware_definition(labware_name) deck = Deck() lw1 = labware.load(labware_name, deck.position_for(1)) lw2 = labware.load(labware_name, deck.position_for(2)) deck[1] = lw1 deck[2] = lw2 # Various flavors of locations without labware should work no_lw = lw1.wells()[0].top()._replace(labware=None) no_from = plan_moves(no_lw, lw2.wells()[0].bottom(), deck, P300M_GEN2_MAX_HEIGHT, 7.0, 15.0) check_arc_basic(no_from, no_lw, lw2.wells()[0].bottom()) assert no_from[0][0].z == deck.highest_z + 15.0 no_to = plan_moves(lw1.wells()[0].bottom(), no_lw, deck, P300M_GEN2_MAX_HEIGHT, 7.0, 15.0) check_arc_basic(no_to, lw1.wells()[0].bottom(), no_lw) assert no_from[0][0].z == deck.highest_z + 15.0 no_well = lw1.wells()[0].top()._replace(labware=lw1) no_from_well = plan_moves(no_well, lw1.wells()[1].top(), deck, P300M_GEN2_MAX_HEIGHT, 7.0, 15.0) check_arc_basic(no_from_well, no_well, lw1.wells()[1].top()) no_from_well_height = no_from_well[0][0].z lw_height_expected = labware_def['dimensions']['zDimension'] + 7 assert no_from_well_height == lw_height_expected no_to_well = plan_moves(lw1.wells()[1].top(), no_well, deck, P300M_GEN2_MAX_HEIGHT, 7.0, 15.0) check_arc_basic(no_to_well, lw1.wells()[1].top(), no_well) no_to_well_height = no_to_well[0][0].z assert no_to_well_height == lw_height_expected
def test_slot_names(): slots_by_int = list(range(1, 13)) slots_by_str = [str(idx) for idx in slots_by_int] for method in (slots_by_int, slots_by_str): d = Deck() for idx, slot in enumerate(method): lw = labware.load(labware_name, d.position_for(slot)) assert slot in d d[slot] = lw with pytest.raises(ValueError): d[slot] = 'not this time boyo' del d[slot] assert slot in d assert d[slot] is None mod = module_geometry.load_module( module_geometry.TemperatureModuleModel.TEMPERATURE_V1, d.position_for(slot)) d[slot] = mod assert mod == d[slot] assert 'hasasdaia' not in d with pytest.raises(ValueError): d['ahgoasia'] = 'nope'
def test_force_direct(): deck = Deck() lw1 = labware.load(labware_name, deck.position_for(1)) lw2 = labware.load(labware_name, deck.position_for(2)) # same-labware moves should move direct same_lw = plan_moves(lw1.wells()[0].top(), lw1.wells()[8].bottom(), deck, P300M_GEN2_MAX_HEIGHT, 7.0, 15.0, force_direct=True) assert same_lw == [(lw1.wells()[8].bottom().point, None)] # different-labware moves should move direct different_lw = plan_moves(lw1.wells()[0].top(), lw2.wells()[0].bottom(), deck, P300M_GEN2_MAX_HEIGHT, 7.0, 15.0, force_direct=True) assert different_lw == [(lw2.wells()[0].bottom().point, None)]
def test_basic_arc(): deck = Deck() lw1 = labware.load(labware_name, deck.position_for(1)) lw2 = labware.load(labware_name, deck.position_for(2)) deck[1] = lw1 deck[2] = lw2 # same-labware moves should use the smaller safe z same_lw = plan_moves(lw1.wells()[0].top(), lw1.wells()[8].bottom(), deck, P300M_GEN2_MAX_HEIGHT, 7.0, 15.0) check_arc_basic(same_lw, lw1.wells()[0].top(), lw1.wells()[8].bottom()) assert same_lw[0][0].z == lw1.wells()[0].top().point.z + 7.0 # different-labware moves, or moves with no labware attached, # should use the larger safe z and the global z different_lw = plan_moves(lw1.wells()[0].top(), lw2.wells()[0].bottom(), deck, P300M_GEN2_MAX_HEIGHT, 7.0, 15.0) check_arc_basic(different_lw, lw1.wells()[0].top(), lw2.wells()[0].bottom()) assert different_lw[0][0].z == deck.highest_z + 15.0
def should_dodge_thermocycler( deck: Deck, from_loc: types.Location, to_loc: types.Location) -> bool: """ Decide if the requested path would cross the thermocycler, if installed. Returns True if we need to dodge, False otherwise """ if any([isinstance(item, ThermocyclerGeometry) for item in deck.values()]): transit = (first_parent(from_loc.labware), first_parent(to_loc.labware)) # mypy doesn't like this because transit could be none, but it's # checked by value in BAD_PAIRS which has only strings return transit in BAD_PAIRS return False
def test_highest_z(): deck = Deck() fixed_trash = deck.get_fixed_trash() assert deck.highest_z == fixed_trash.highest_z tall_lw = labware.load(tall_lw_name, deck.position_for(1)) deck[1] = tall_lw assert deck.highest_z == pytest.approx(tall_lw.wells()[0].top().point.z) del deck[1] assert deck.highest_z == fixed_trash.highest_z mod = module_geometry.load_module( module_geometry.TemperatureModuleModel.TEMPERATURE_V1, deck.position_for(8)) deck[8] = mod assert deck.highest_z == mod.highest_z lw = labware.load(labware_name, mod.location) mod.add_labware(lw) deck.recalculate_high_z() assert deck.highest_z == mod.highest_z
def test_gen2_module_transforms(): deck = Deck() tmod = module_geometry.load_module( module_geometry.TemperatureModuleModel.TEMPERATURE_V2, deck.position_for('1'), MAX_SUPPORTED_VERSION) assert tmod.labware_offset == Point(-1.45, -0.15, 80.09) tmod2 = module_geometry.load_module( module_geometry.TemperatureModuleModel.TEMPERATURE_V2, deck.position_for('3'), MAX_SUPPORTED_VERSION) assert tmod2.labware_offset == Point(1.15, -0.15, 80.09) mmod = module_geometry.load_module( module_geometry.MagneticModuleModel.MAGNETIC_V2, deck.position_for('1'), MAX_SUPPORTED_VERSION) assert mmod.labware_offset == Point(-1.175, -0.125, 82.25) mmod2 = module_geometry.load_module( module_geometry.MagneticModuleModel.MAGNETIC_V2, deck.position_for('3'), MAX_SUPPORTED_VERSION) assert mmod2.labware_offset == Point(1.425, -0.125, 82.25)
class ProtocolContext(CommandPublisher): """ The Context class is a container for the state of a protocol. It encapsulates many of the methods formerly found in the Robot class, including labware, instrument, and module loading, as well as core functions like pause and resume. Unlike the old robot class, it is designed to be ephemeral. The lifetime of a particular instance should be about the same as the lifetime of a protocol. The only exception is the one stored in :py:attr:`.legacy_api.api.robot`, which is provided only for back compatibility and should be used less and less as time goes by. .. versionadded:: 2.0 """ def __init__( self, loop: asyncio.AbstractEventLoop = None, hardware: HardwareToManage = None, broker=None, bundled_labware: Dict[str, 'LabwareDefinition'] = None, bundled_data: Dict[str, bytes] = None, extra_labware: Dict[str, 'LabwareDefinition'] = None, api_version: APIVersion = None, ) -> None: """ Build a :py:class:`.ProtocolContext`. :param loop: An event loop to use. If not specified, this ctor will (eventually) call :py:meth:`asyncio.get_event_loop`. :param hardware: An optional hardware controller to link to. If not specified, a new simulator will be created. :param broker: An optional command broker to link to. If not specified, a dummy one is used. :param bundled_labware: A dict mapping labware URIs to definitions. This is used when executing bundled protocols, and if specified will be the only allowed source for labware definitions, excluding the built in definitions and anything in ``extra_labware``. :param bundled_data: A dict mapping filenames to the contents of data files. Can be used by the protocol, since it is exposed as :py:attr:`.ProtocolContext.bundled_data` :param extra_labware: A dict mapping labware URIs to definitions. These URIs are searched during :py:meth:`.load_labware` in addition to the system definitions (if ``bundled_labware`` was not specified). Used to provide custom labware definitions. :param api_version: The API version to use. If this is ``None``, uses the max supported version. """ super().__init__(broker) self._api_version = api_version or MAX_SUPPORTED_VERSION if self._api_version > MAX_SUPPORTED_VERSION: raise RuntimeError( f'API version {self._api_version} is not supported by this ' f'robot software. Please either reduce your requested API ' f'version or update your robot.') self._loop = loop or asyncio.get_event_loop() deck_load_name = SHORT_TRASH_DECK if fflags.short_fixed_trash() \ else STANDARD_DECK self._deck_layout = Deck(load_name=deck_load_name) self._instruments: Dict[types.Mount, Optional[InstrumentContext]]\ = {mount: None for mount in types.Mount} self._modules: Set[ModuleContext] = set() self._last_moved_instrument: Optional[types.Mount] = None self._location_cache: Optional[types.Location] = None self._hw_manager = HardwareManager(hardware) self._log = MODULE_LOG.getChild(self.__class__.__name__) self._commands: List[str] = [] self._unsubscribe_commands = None self.clear_commands() self._bundled_labware = bundled_labware self._extra_labware = extra_labware or {} self._bundled_data: Dict[str, bytes] = bundled_data or {} self._default_max_speeds = AxisMaxSpeeds() @classmethod def build_using(cls, protocol: Protocol, *args, **kwargs): """ Build an API instance for the specified parsed protocol This is used internally to provision the context with bundle contents or api levels. """ kwargs['bundled_data'] = getattr(protocol, 'bundled_data', None) kwargs['bundled_labware'] = getattr(protocol, 'bundled_labware', None) kwargs['api_version'] = getattr(protocol, 'api_level', MAX_SUPPORTED_VERSION) return cls(*args, **kwargs) @property # type: ignore @requires_version(2, 0) def api_version(self) -> APIVersion: """ Return the API version supported by this protocol context. The supported API version was specified when the protocol context was initialized. It may be lower than the highest version supported by the robot software. For the highest version supported by the robot software, see :py:attr:`.protocol_api.MAX_SUPPORTED_VERSION`. """ return self._api_version @property # type: ignore @requires_version(2, 0) def bundled_data(self) -> Dict[str, bytes]: """ Accessor for data files bundled with this protocol, if any. This is a dictionary mapping the filenames of bundled datafiles, with extensions but without paths (e.g. if a file is stored in the bundle as ``data/mydata/aspirations.csv`` it will be in the dict as ``'aspirations.csv'``) to the bytes contents of the files. """ return self._bundled_data def cleanup(self): """ Finalize and clean up the protocol context. """ if self._unsubscribe_commands: self._unsubscribe_commands() self._unsubscribe_commands = None def __del__(self): if getattr(self, '_unsubscribe_commands', None): self._unsubscribe_commands() # type: ignore @property # type: ignore @requires_version(2, 0) def max_speeds(self) -> AxisMaxSpeeds: """ Per-axis speed limits when moving this instrument. Changing this value changes the speed limit for each non-plunger axis of the robot, when moving this pipette. Note that this does only sets a limit on how fast movements can be; movements can still be slower than this. However, it is useful if you require the robot to move much more slowly than normal when using this pipette. This is a dictionary mapping string names of axes to float values limiting speeds. To change a speed, set that axis's value. To reset an axis's speed to default, delete the entry for that axis or assign it to ``None``. For instance, .. code-block:: py def run(protocol): protocol.comment(str(right.max_speeds)) # '{}' - all default protocol.max_speeds['A'] = 10 # limit max speed of # right pipette Z to 10mm/s del protocol.max_speeds['A'] # reset to default protocol.max_speeds['X'] = 10 # limit max speed of x to # 10 mm/s protocol.max_speeds['X'] = None # reset to default """ return self._default_max_speeds @requires_version(2, 0) def commands(self): return self._commands @requires_version(2, 0) def clear_commands(self): self._commands.clear() if self._unsubscribe_commands: self._unsubscribe_commands() def on_command(message): payload = message.get('payload') text = payload.get('text') if text is None: return if message['$'] == 'before': self._commands.append(text.format(**payload)) self._unsubscribe_commands = self.broker.subscribe( cmds.types.COMMAND, on_command) @contextlib.contextmanager def temp_connect(self, hardware: API): """ Connect temporarily to the specified hardware controller. This should be used as a context manager: .. code-block :: python with ctx.temp_connect(hw): # do some tasks ctx.home() # after the with block, the context is connected to the same # hardware control API it was connected to before, even if # an error occurred in the code inside the with block """ old_hw = self._hw_manager.hardware old_tc = None tc_context = None try: self._hw_manager.set_hw(hardware) for mod_ctx in self._modules: if isinstance(mod_ctx, ThermocyclerContext): tc_context = mod_ctx hw_tc = next(hw_mod for hw_mod in hardware.attached_modules if hw_mod.name() == 'thermocycler') if hw_tc: old_tc = mod_ctx._module mod_ctx._module = hw_tc yield self finally: self._hw_manager.set_hw(old_hw) if tc_context is not None and old_tc is not None: tc_context._module = old_tc @requires_version(2, 0) def connect(self, hardware: API): """ Connect to a running hardware API. This can be either a simulator or a full hardware controller. Note that there is no true disconnected state for a :py:class:`.ProtocolContext`; :py:meth:`disconnect` simply creates a new simulator and replaces the current hardware with it. """ self._hw_manager.set_hw(hardware) self._hw_manager.hardware.cache_instruments() @requires_version(2, 0) def disconnect(self): """ Disconnect from currently-connected hardware and simulate instead """ self._hw_manager.reset_hw() @requires_version(2, 0) def is_simulating(self) -> bool: return self._hw_manager.hardware.is_simulator @requires_version(2, 0) def load_labware_from_definition( self, labware_def: 'LabwareDefinition', location: types.DeckLocation, label: str = None, ) -> Labware: """ Specify the presence of a piece of labware on the OT2 deck. This function loads the labware definition specified by `labware_def` to the location specified by `location`. :param labware_def: The labware definition to load :param location: The slot into which to load the labware such as 1 or '1' :type location: int or str :param str label: An optional special name to give the labware. If specified, this is the name the labware will appear as in the run log and the calibration view in the Opentrons app. """ parent = self.deck.position_for(location) labware_obj = load_from_definition(labware_def, parent, label) self._deck_layout[location] = labware_obj return labware_obj @requires_version(2, 0) def load_labware( self, load_name: str, location: types.DeckLocation, label: str = None, namespace: str = None, version: int = None, ) -> Labware: """ Load a labware onto the deck given its name. For labware already defined by Opentrons, this is a convenient way to collapse the two stages of labware initialization (creating the labware and adding it to the protocol) into one. This function returns the created and initialized labware for use later in the protocol. :param load_name: A string to use for looking up a labware definition :param location: The slot into which to load the labware such as 1 or '1' :type location: int or str :param str label: An optional special name to give the labware. If specified, this is the name the labware will appear as in the run log and the calibration view in the Opentrons app. :param str namespace: The namespace the labware definition belongs to. If unspecified, will search 'opentrons' then 'custom_beta' :param int version: The version of the labware definition. If unspecified, will use version 1. """ labware_def = get_labware_definition( load_name, namespace, version, bundled_defs=self._bundled_labware, extra_defs=self._extra_labware) return self.load_labware_from_definition(labware_def, location, label) @requires_version(2, 0) def load_labware_by_name(self, load_name: str, location: types.DeckLocation, label: str = None, namespace: str = None, version: int = 1) -> Labware: MODULE_LOG.warning( 'load_labware_by_name is deprecated and will be removed in ' 'version 3.12.0. please use load_labware') return self.load_labware(load_name, location, label, namespace, version) @property # type: ignore @requires_version(2, 0) def loaded_labwares(self) -> Dict[int, Union[Labware, ModuleGeometry]]: """ Get the labwares that have been loaded into the protocol context. Slots with nothing in them will not be present in the return value. .. note:: If a module is present on the deck but no labware has been loaded into it with :py:meth:`.ModuleContext.load_labware`, there will be no entry for that slot in this value. That means you should not use ``loaded_labwares`` to determine if a slot is available or not, only to get a list of labwares. If you want a data structure of all objects on the deck regardless of type, see :py:attr:`deck`. :returns: Dict mapping deck slot number to labware, sorted in order of the locations. """ def _only_labwares( ) -> Iterator[Tuple[int, Union[Labware, ModuleGeometry]]]: for slotnum, slotitem in self._deck_layout.items(): if isinstance(slotitem, Labware): yield slotnum, slotitem elif isinstance(slotitem, ModuleGeometry): if slotitem.labware: yield slotnum, slotitem.labware return dict(_only_labwares()) @requires_version(2, 0) def load_module(self, module_name: str, location: Optional[types.DeckLocation] = None, configuration: str = None) -> ModuleTypes: """ Load a module onto the deck given its name. This is the function to call to use a module in your protocol, like :py:meth:`load_instrument` is the method to call to use an instrument in your protocol. It returns the created and initialized module context, which will be a different class depending on the kind of module loaded. A map of deck positions to loaded modules can be accessed later using :py:attr:`loaded_modules`. :param str module_name: The name or model of the module. :param location: The location of the module. This is usually the name or number of the slot on the deck where you will be placing the module. Some modules, like the Thermocycler, are only valid in one deck location. You do not have to specify a location when loading a Thermocycler - it will always be in Slot 7. :param configuration: Used to specify the slot configuration of the Thermocycler. Only Valid in Python API Version 2.4 and later. If you wish to use the non-full plate configuration, you must pass in the key word value `semi` :type location: str or int or None :returns ModuleContext: The loaded and initialized :py:class:`ModuleContext`. """ resolved_model = resolve_module_model(module_name) resolved_type = resolve_module_type(resolved_model) resolved_location = self._deck_layout.resolve_module_location( resolved_type, location) if self._api_version < APIVersion(2, 4) and configuration: raise APIVersionError( f'You have specified API {self._api_version}, but you are' 'using thermocycler parameters only available in 2.4') geometry = load_module( resolved_model, self._deck_layout.position_for(resolved_location), self._api_version, configuration) hc_mod_instance = None mod_class = { ModuleType.MAGNETIC: MagneticModuleContext, ModuleType.TEMPERATURE: TemperatureModuleContext, ModuleType.THERMOCYCLER: ThermocyclerContext }[resolved_type] for mod in self._hw_manager.hardware.attached_modules: if models_compatible(module_model_from_string(mod.model()), resolved_model): hc_mod_instance = SynchronousAdapter(mod) break if self.is_simulating() and hc_mod_instance is None: mod_type = { ModuleType.MAGNETIC: modules.magdeck.MagDeck, ModuleType.TEMPERATURE: modules.tempdeck.TempDeck, ModuleType.THERMOCYCLER: modules.thermocycler.Thermocycler }[resolved_type] hc_mod_instance = SynchronousAdapter( mod_type(port='', simulating=True, loop=self._hw_manager.hardware.loop, execution_manager=ExecutionManager( loop=self._hw_manager.hardware.loop), sim_model=resolved_model.value)) hc_mod_instance._connect() if hc_mod_instance: mod_ctx = mod_class(self, hc_mod_instance, geometry, self.api_version, self._loop) else: raise RuntimeError( f'Could not find specified module: {module_name}') self._modules.add(mod_ctx) self._deck_layout[resolved_location] = geometry return mod_ctx @property # type: ignore @requires_version(2, 0) def loaded_modules(self) -> Dict[int, 'ModuleContext']: """ Get the modules loaded into the protocol context. This is a map of deck positions to modules loaded by previous calls to :py:meth:`load_module`. It is not necessarily the same as the modules attached to the robot - for instance, if the robot has a Magnetic Module and a Temperature Module attached, but the protocol has only loaded the Temperature Module with :py:meth:`load_module`, only the Temperature Module will be present. :returns Dict[str, ModuleContext]: Dict mapping slot name to module contexts. The elements may not be ordered by slot number. """ def _modules() -> Iterator[Tuple[int, 'ModuleContext']]: for module in self._modules: yield int(module.geometry.parent), module return dict(_modules()) @requires_version(2, 0) def load_instrument(self, instrument_name: str, mount: Union[types.Mount, str], tip_racks: List[Labware] = None, replace: bool = False) -> 'InstrumentContext': """ Load a specific instrument required by the protocol. This value will actually be checked when the protocol runs, to ensure that the correct instrument is attached in the specified location. :param str instrument_name: The name of the instrument model, or a prefix. For instance, 'p10_single' may be used to request a P10 single regardless of the version. :param mount: The mount in which this instrument should be attached. This can either be an instance of the enum type :py:class:`.types.Mount` or one of the strings `'left'` and `'right'`. :type mount: types.Mount or str :param tip_racks: A list of tip racks from which to pick tips if :py:meth:`.InstrumentContext.pick_up_tip` is called without arguments. :type tip_racks: List[:py:class:`.Labware`] :param bool replace: Indicate that the currently-loaded instrument in `mount` (if such an instrument exists) should be replaced by `instrument_name`. """ if isinstance(mount, str): try: checked_mount = types.Mount[mount.upper()] except KeyError: raise ValueError( "If mount is specified as a string, it should be either" "'left' or 'right' (ignoring capitalization, which the" " system strips), not {}".format(mount)) elif isinstance(mount, types.Mount): checked_mount = mount else: raise TypeError( "mount should be either an instance of opentrons.types.Mount" " or a string, but is {}.".format(mount)) self._log.info("Trying to load {} on {} mount".format( instrument_name, checked_mount.name.lower())) instr = self._instruments[checked_mount] if instr and not replace: raise RuntimeError( "Instrument already present in {} mount: {}".format( checked_mount.name.lower(), instr.name)) attached = { att_mount: instr.get('name', None) for att_mount, instr in self._hw_manager.hardware.attached_instruments.items() if instr } attached[checked_mount] = instrument_name self._log.debug("cache instruments expectation: {}".format(attached)) self._hw_manager.hardware.cache_instruments(attached) # If the cache call didn’t raise, the instrument is attached new_instr = InstrumentContext(ctx=self, hardware_mgr=self._hw_manager, mount=checked_mount, at_version=self._api_version, tip_racks=tip_racks, log_parent=self._log, requested_as=instrument_name) self._instruments[checked_mount] = new_instr self._log.info("Instrument {} loaded".format(new_instr)) return new_instr @property # type: ignore @requires_version(2, 0) def loaded_instruments(self) -> Dict[str, Optional['InstrumentContext']]: """ Get the instruments that have been loaded into the protocol. This is a map of mount name to instruments previously loaded with :py:meth:`load_instrument`. It is not necessarily the same as the instruments attached to the robot - for instance, if the robot has an instrument in both mounts but your protocol has only loaded one of them with :py:meth:`load_instrument`, the unused one will not be present. :returns: A dict mapping mount names in lowercase to the instrument in that mount. If no instrument is loaded in the mount, it will not be present """ return { mount.name.lower(): instr for mount, instr in self._instruments.items() if instr } @cmds.publish.both(command=cmds.pause) @requires_version(2, 0) def pause(self, msg=None): """ Pause execution of the protocol until resume is called. This function returns immediately, but the next function call that is blocked by a paused robot (anything that involves moving) will not return until :py:meth:`resume` is called. :param str msg: A message to echo back to connected clients. """ self._hw_manager.hardware.pause() @cmds.publish.both(command=cmds.resume) @requires_version(2, 0) def resume(self): """ Resume a previously-paused protocol """ self._hw_manager.hardware.resume() @cmds.publish.both(command=cmds.comment) @requires_version(2, 0) def comment(self, msg): """ Add a user-readable comment string that will be echoed to the Opentrons app. The value of the message is computed during protocol simulation, so cannot be used to communicate real-time information from the robot's actual run. """ pass @cmds.publish.both(command=cmds.delay) @requires_version(2, 0) def delay(self, seconds=0, minutes=0, msg=None): """ Delay protocol execution for a specific amount of time. :param float seconds: A time to delay in seconds :param float minutes: A time to delay in minutes If both `seconds` and `minutes` are specified, they will be added. """ delay_time = seconds + minutes * 60 self._hw_manager.hardware.delay(delay_time) @requires_version(2, 0) def home(self): """ Homes the robot. """ self._log.debug("home") self._location_cache = None self._hw_manager.hardware.home() @property def location_cache(self) -> Optional[types.Location]: """ The cache used by the robot to determine where it last was. """ return self._location_cache @location_cache.setter def location_cache(self, loc: Optional[types.Location]): self._location_cache = loc @property # type: ignore @requires_version(2, 0) def deck(self) -> Deck: """ The object holding the deck layout of the robot. This object behaves like a dictionary with keys for both numeric and string slot numbers (for instance, ``protocol.deck[1]`` and ``protocol.deck['1']`` will both return the object in slot 1). If nothing is loaded into a slot, ``None`` will be present. This object is useful for determining if a slot in the deck is free. Rather than filtering the objects in the deck map yourself, you can also use :py:attr:`loaded_labwares` to see a dict of labwares and :py:attr:`loaded_modules` to see a dict of modules. For advanced control you can delete an item of labware from the deck with e.g. ``del protocol.deck['1']`` to free a slot for new labware. (Note that for each slot only the last labware used in a command will be available for calibration in the OpenTrons UI, and that the tallest labware on the deck will be calculated using only currently loaded labware, meaning that the labware loaded should always reflect the labware physically on the deck (or be higher than the labware on the deck). """ return self._deck_layout @property # type: ignore @requires_version(2, 0) def fixed_trash(self) -> Labware: """ The trash fixed to slot 12 of the robot deck. It has one well and should be accessed like labware in your protocol. e.g. ``protocol.fixed_trash['A1']`` """ trash = self._deck_layout['12'] if not trash: raise RuntimeError("Robot must have a trash container in 12") return trash # type: ignore @requires_version(2, 5) def set_rail_lights(self, on: bool): """ Controls the robot rail lights :param bool on: If true, turn on rail lights; otherwise, turn off. """ self._hw_manager.hardware.set_lights(rails=on) @property # type: ignore @requires_version(2, 5) def rail_lights_on(self) -> bool: """ Returns True if the rail lights are on """ return self._hw_manager.hardware.get_lights()['rails'] @property # type: ignore @requires_version(2, 5) def door_closed(self) -> bool: """ Returns True if the robot door is closed """ return convert_door_state_to_bool(self._hw_manager.hardware.door_state)
def plan_moves( from_loc: types.Location, to_loc: types.Location, deck: Deck, instr_max_height: float, well_z_margin: float = None, lw_z_margin: float = None, force_direct: bool = False, minimum_lw_z_margin: float = None, minimum_z_height: float = None,)\ -> List[Tuple[types.Point, Optional[CriticalPoint]]]: """ Plan moves between one :py:class:`.Location` and another. Each :py:class:`.Location` instance might or might not have a specific kind of geometry attached. This function is intended to return series of moves that contain the minimum safe retractions to avoid (known) labware on the specified :py:class:`Deck`. :param from_loc: The last location. :param to_loc: The location to move to. :param deck: The :py:class:`Deck` instance describing the robot. :param force_direct: If True, ignore any Z margins force a direct move The other parameters are as :py:meth:`safe_height`. :returns: A list of tuples of :py:class:`.Point` and critical point overrides to move through. """ constraints = MoveConstraints.build( instr_max_height=instr_max_height, well_z_margin=well_z_margin, lw_z_margin=lw_z_margin, minimum_lw_z_margin=minimum_lw_z_margin, minimum_z_height=minimum_z_height) assert constraints.minimum_z_height >= 0.0 to_point = to_loc.point to_lw, to_well = split_loc_labware(to_loc) from_point = from_loc.point from_lw, from_well = split_loc_labware(from_loc) dest_quirks = quirks_from_any_parent(to_lw) from_quirks = quirks_from_any_parent(from_lw) from_center = 'centerMultichannelOnWells' in from_quirks to_center = 'centerMultichannelOnWells' in dest_quirks dest_cp_override = CriticalPoint.XY_CENTER if to_center else None origin_cp_override = CriticalPoint.XY_CENTER if from_center else None is_same_location = ((to_lw and to_lw == from_lw) and (to_well and to_well == from_well)) if (force_direct or (is_same_location and not (minimum_z_height or 0) > 0)): # If we’re going direct, we can assume we’re already in the correct # cp so we can use the override without prep return [(to_point, dest_cp_override)] # Generate arc moves # Find the safe z heights based on the destination and origin labware/well safe = _build_safe_height(from_loc, to_loc, deck, constraints) must_dodge = should_dodge_thermocycler(deck, from_loc, to_loc) if must_dodge: sc = deck.get_slot_center('5') wp = [(sc.x, sc.y)] else: wp = [] return plan_arc(from_point, to_point, safe, origin_cp_override, dest_cp_override, wp)
def test_get_non_fixture_slots(): deck = Deck() trough = labware.load(trough_name, deck.position_for(4)) deck[4] = trough assert deck.get_non_fixture_slots() == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
def __init__( self, loop: asyncio.AbstractEventLoop = None, hardware: HardwareToManage = None, broker=None, bundled_labware: Dict[str, 'LabwareDefinition'] = None, bundled_data: Dict[str, bytes] = None, extra_labware: Dict[str, 'LabwareDefinition'] = None, api_version: APIVersion = None, ) -> None: """ Build a :py:class:`.ProtocolContext`. :param loop: An event loop to use. If not specified, this ctor will (eventually) call :py:meth:`asyncio.get_event_loop`. :param hardware: An optional hardware controller to link to. If not specified, a new simulator will be created. :param broker: An optional command broker to link to. If not specified, a dummy one is used. :param bundled_labware: A dict mapping labware URIs to definitions. This is used when executing bundled protocols, and if specified will be the only allowed source for labware definitions, excluding the built in definitions and anything in ``extra_labware``. :param bundled_data: A dict mapping filenames to the contents of data files. Can be used by the protocol, since it is exposed as :py:attr:`.ProtocolContext.bundled_data` :param extra_labware: A dict mapping labware URIs to definitions. These URIs are searched during :py:meth:`.load_labware` in addition to the system definitions (if ``bundled_labware`` was not specified). Used to provide custom labware definitions. :param api_version: The API version to use. If this is ``None``, uses the max supported version. """ super().__init__(broker) self._api_version = api_version or MAX_SUPPORTED_VERSION if self._api_version > MAX_SUPPORTED_VERSION: raise RuntimeError( f'API version {self._api_version} is not supported by this ' f'robot software. Please either reduce your requested API ' f'version or update your robot.') self._loop = loop or asyncio.get_event_loop() deck_load_name = SHORT_TRASH_DECK if fflags.short_fixed_trash() \ else STANDARD_DECK self._deck_layout = Deck(load_name=deck_load_name) self._instruments: Dict[types.Mount, Optional[InstrumentContext]]\ = {mount: None for mount in types.Mount} self._modules: Set[ModuleContext] = set() self._last_moved_instrument: Optional[types.Mount] = None self._location_cache: Optional[types.Location] = None self._hw_manager = HardwareManager(hardware) self._log = MODULE_LOG.getChild(self.__class__.__name__) self._commands: List[str] = [] self._unsubscribe_commands = None self.clear_commands() self._bundled_labware = bundled_labware self._extra_labware = extra_labware or {} self._bundled_data: Dict[str, bytes] = bundled_data or {} self._default_max_speeds = AxisMaxSpeeds()
def test_should_dodge(): deck = Deck() # with no tc loaded, doesn't matter what the positions are assert not should_dodge_thermocycler(deck, deck.position_for(4), deck.position_for(9)) deck[7] = module_geometry.load_module( module_geometry.ThermocyclerModuleModel.THERMOCYCLER_V1, deck.position_for(7)) # with a tc loaded, some positions should require dodging assert should_dodge_thermocycler(deck, deck.position_for(12), deck.position_for(1)) assert should_dodge_thermocycler(deck, deck.position_for(9), deck.position_for(4)) # but ones that weren't explicitly marked shouldn't assert not should_dodge_thermocycler(deck, deck.position_for(1), deck.position_for(2)) # including a situation where we might have some messed up locations # with no parent assert not should_dodge_thermocycler( deck, deck.position_for(1)._replace(labware=None), deck.position_for(12))