Ejemplo n.º 1
0
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())
Ejemplo n.º 2
0
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))
Ejemplo n.º 3
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)]
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
0
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
Ejemplo n.º 6
0
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())
Ejemplo n.º 7
0
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)
Ejemplo n.º 8
0
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)
Ejemplo n.º 9
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
Ejemplo n.º 10
0
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
Ejemplo n.º 11
0
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
Ejemplo n.º 12
0
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
Ejemplo n.º 13
0
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'
Ejemplo n.º 14
0
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)]
Ejemplo n.º 15
0
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
Ejemplo n.º 16
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
Ejemplo n.º 17
0
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
Ejemplo n.º 18
0
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)
Ejemplo n.º 19
0
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)
Ejemplo n.º 20
0
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)
Ejemplo n.º 21
0
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]
Ejemplo n.º 22
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()
Ejemplo n.º 23
0
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))