Example #1
0
    def touch_tip(self, location: Optional[Well], radius: float,
                  v_offset: float, speed: float):
        if location is None:
            if not self._ctx.location_cache:
                raise RuntimeError('No valid current location cache present')
            else:
                location = self._ctx.location_cache.labware  # type: ignore
                # type checked below
        if isinstance(location, Well):
            if 'touchTipDisabled' in quirks_from_any_parent(location):
                self._log.info(f"Ignoring touch tip on labware {location}")
                return self
            if location.parent.is_tiprack:
                self._log.warning('Touch_tip being performed on a tiprack. '
                                  'Please re-check your code')

            move_with_z_offset =\
                location.top().point + types.Point(0, 0, v_offset)
            to_loc = types.Location(move_with_z_offset, location)
            self.move_to(to_loc)
        else:
            # If location is a not a valid well, raise a type error
            raise TypeError(
                'location should be a Well, but it is {}'.format(location))

        edges = build_edges(location, v_offset, self._pair_policy.primary,
                            self._ctx._deck_layout, radius)
        for edge in edges:
            self._hw_manager.hardware.move_to(self._pair_policy, edge, speed)
Example #2
0
 def point_and_well(self, location: Union[types.Location, WellV2]):
     if isinstance(location, WellV2):
         labware, point = location.top()
     elif isinstance(location, types.Location):
         labware, point = location
     else:
         raise ValueError("can't unpack")
     return types.Location(point, labware)
Example #3
0
    def move_to(self,
                location: types.Location,
                force_direct: bool = False,
                minimum_z_height: Optional[float] = None,
                speed: Optional[float] = None):
        if not speed:
            speed = self.p_instrument.default_speed
        if self._ctx.location_cache:
            from_lw = self._ctx.location_cache.labware
        else:
            from_lw = None

        from_center = 'centerMultichannelOnWells'\
            in quirks_from_any_parent(from_lw)
        cp_override = CriticalPoint.XY_CENTER if from_center else None
        from_loc = types.Location(
            self._hw_manager.hardware.gantry_position(
                self._pair_policy.primary, critical_point=cp_override),
            from_lw)

        for mod in self._ctx._modules:
            if isinstance(mod, ThermocyclerContext):
                mod.flag_unsafe_move(to_loc=location, from_loc=from_loc)

        primary_height = \
            self._hw_manager.hardware.get_instrument_max_height(
                self._pair_policy.primary)
        secondary_height = \
            self._hw_manager.hardware.get_instrument_max_height(
                self._pair_policy.secondary)
        moves = planning.plan_moves(from_loc,
                                    location,
                                    self._ctx.deck,
                                    min(primary_height, secondary_height),
                                    force_direct=force_direct,
                                    minimum_z_height=minimum_z_height)
        self._log.debug("move_to: {}->{} via:\n\t{}".format(
            from_loc, location, moves))
        try:
            for move in moves:
                self._hw_manager.hardware.move_to(
                    self._pair_policy,
                    move[0],
                    critical_point=move[1],
                    speed=speed,
                    max_speeds=self._ctx.max_speeds.data)
        except Exception:
            self._ctx.location_cache = None
            raise
        else:
            self._ctx.location_cache = location
        return self
 def _prepare_for_lid_move(self):
     loaded_instruments = [
         instr for mount, instr in self._ctx.loaded_instruments.items()
         if instr is not None
     ]
     try:
         instr = loaded_instruments[0]
     except IndexError:
         MODULE_LOG.warning(
             "Cannot assure a safe gantry position to avoid colliding"
             " with the lid of the Thermocycler Module.")
     else:
         self._ctx._hw_manager.hardware.retract(instr._mount)
         high_point = self._ctx._hw_manager.hardware.current_position(
             instr._mount)
         trash_top = self._ctx.fixed_trash.wells()[0].top()
         safe_point = trash_top.point._replace(
             z=high_point[Axis.by_mount(instr._mount)])
         instr.move_to(types.Location(safe_point, None), force_direct=True)
Example #5
0
def run(protocol: protocol_api.ProtocolContext):
    tiprack = protocol.load_labware(TIPRACK_LOADNAME, TIPRACK_SLOT)
    pipette = protocol.load_instrument(PIPETTE_NAME,
                                       PIPETTE_MOUNT,
                                       tip_racks=[tiprack])

    test_labware = protocol.load_labware_from_definition(
        LABWARE_DEF,
        TEST_LABWARE_SLOT,
        LABWARE_LABEL,
    )

    num_cols = len(LABWARE_DEF.get('ordering', [[]]))
    num_rows = len(LABWARE_DEF.get('ordering', [[]])[0])
    well_locs = uniq(
        ['A1', '{}{}'.format(chr(ord('A') + num_rows - 1), str(num_cols))])

    pipette.pick_up_tip()

    def set_speeds(rate):
        protocol.max_speeds.update({
            'X': (600 * rate),
            'Y': (400 * rate),
            'Z': (125 * rate),
            'A': (125 * rate),
        })

        speed_max = max(protocol.max_speeds.values())

        for instr in protocol.loaded_instruments.values():
            instr.default_speed = speed_max

    set_speeds(RATE)

    for slot in CALIBRATION_CROSS_SLOTS:
        coordinate = CALIBRATION_CROSS_COORDS[slot]
        location = types.Location(point=types.Point(**coordinate),
                                  labware=None)
        pipette.move_to(location)
        protocol.pause(
            f"Confirm {PIPETTE_MOUNT} pipette is at slot {slot} calibration cross"
        )

    pipette.home()
    protocol.pause(f"Place your labware in Slot {TEST_LABWARE_SLOT}")

    for well_loc in well_locs:
        well = test_labware.well(well_loc)
        all_4_edges = [[well._from_center_cartesian(x=-1, y=0, z=1), 'left'],
                       [well._from_center_cartesian(x=1, y=0, z=1), 'right'],
                       [well._from_center_cartesian(x=0, y=-1, z=1), 'front'],
                       [well._from_center_cartesian(x=0, y=1, z=1), 'back']]

        set_speeds(RATE)
        pipette.move_to(well.top())
        protocol.pause("Moved to the top of the well")

        for edge_pos, edge_name in all_4_edges:
            set_speeds(SLOWER_RATE)
            edge_location = types.Location(point=edge_pos, labware=None)
            pipette.move_to(edge_location)
            protocol.pause(f'Moved to {edge_name} edge')

        set_speeds(RATE)
        pipette.move_to(well.bottom())
        protocol.pause("Moved to the bottom of the well")

        pipette.blow_out(well)

    set_speeds(1.0)
    pipette.return_tip()
Example #6
0
 def position_for(self, key: types.DeckLocation) -> types.Location:
     key_int = self._check_name(key)
     return types.Location(self._positions[key_int], str(key))
 def move_to_point(self, coordinates):
     point = types.Point(*coordinates)
     self.instr.move_to(location=types.Location(point=point, labware=None),
                        minimum_z_height=SAFE_Z_HEIGHT)
Example #8
0
    def dispense(
            self,
            volume=None,
            location=None,
            rate=1.0,
            # remainder are added params
            full_dispense: bool = False,
            top_clearance=None,
            bottom_clearance=None,
            manual_liquid_volume_allowance=None):
        # figure out where we're dispensing to
        # recapitulate super
        if isinstance(location, WellV2):
            if 'fixedTrash' in quirks_from_any_parent(location):
                loc = location.top()
            else:
                point, well = location.bottom()
                loc = types.Location(
                    point +
                    types.Point(0, 0, self.well_bottom_clearance.dispense),
                    well)
            self.move_to(loc)
        elif isinstance(location, types.Location):
            loc = location
            self.move_to(location)
        elif location is not None:
            raise TypeError(
                'location should be a Well or Location, but it is {}'.format(
                    location))
        elif self._ctx.location_cache:
            loc = self._ctx.location_cache
        else:
            raise RuntimeError(
                "If dispense is called without an explicit location, another method that moves to a location (such as move_to or aspirate) must previously have been called so the robot knows where it is."
            )

        location = loc  # no need for new variable
        assert isinstance(location, types.Location)
        point, well = location

        if top_clearance is None:
            if tls.dispense_params_transfer:
                top_clearance = tls.dispense_params_transfer.top_clearance_transfer
            if top_clearance is None:
                top_clearance = self.well_top_clearance.dispense
        if bottom_clearance is None:
            if tls.dispense_params_transfer:
                bottom_clearance = tls.dispense_params_transfer.bottom_clearance_transfer
            if bottom_clearance is None:
                bottom_clearance = self.well_bottom_clearance.dispense
        if manual_liquid_volume_allowance is None:
            if tls.dispense_params_transfer:
                manual_liquid_volume_allowance = tls.dispense_params_transfer.manual_manufacture_tolerance_transfer
            if manual_liquid_volume_allowance is None:
                manual_liquid_volume_allowance = self.config.dispense.manual_liquid_volume_allowance

        if is_close(volume, self.current_volume
                    ):  # avoid finicky floating-point precision issues
            volume = self.current_volume
        location = self._adjust_location_to_liquid_top(
            location=location,
            aspirate_volume=None,
            top_clearance=top_clearance,
            bottom_clearance=bottom_clearance,
            manual_liquid_volume_allowance=manual_liquid_volume_allowance)

        with DispenseParams():
            tls.dispense_params.full_dispense_from_dispense = full_dispense

            def call_super():
                super(EnhancedPipette, self).dispense(volume=volume,
                                                      location=location,
                                                      rate=rate)

            self.use_self_while(call_super)

            if tls.dispense_params.fully_dispensed:
                assert self.current_volume == 0
                if self.current_volume == 0:
                    pass  # nothing to do: the next self._position_for_aspirate will reposition for us: 'if pipette is currently empty, ensure the plunger is at "bottom"'
                else:
                    raise NotImplementedError

            # track volume
            well.liquid_volume.dispense(volume)
Example #9
0
    def aspirate(
            self,
            volume: float = None,
            location: Union[types.Location, EnhancedWellV2] = None,
            rate: float = 1.0,
            # remainder are added params
            pre_wet: bool = None,
            ms_pause: float = None,
            top_clearance=None,
            bottom_clearance=None,
            manual_liquid_volume_allowance=None):

        # figure out where we're aspirating from
        # recapitulate super
        if isinstance(location, WellV2):
            point, well = location.bottom()
            dest = types.Location(
                point + types.Point(0, 0, self.well_bottom_clearance.aspirate),
                well)
        elif isinstance(location, types.Location):
            dest = location
        elif location is not None:
            raise TypeError(
                'location should be a Well or Location, but it is {}'.format(
                    location))
        elif self._ctx.location_cache:
            dest = self._ctx.location_cache
        else:
            raise RuntimeError(
                "If aspirate is called without an explicit location, another method that moves to a  location (such as move_to or dispense) must previously have been called so the robot knows where it is."
            )

        location = dest  # no need for new variable
        assert isinstance(location, types.Location)
        point, well = location

        if top_clearance is None:
            if tls.aspirate_params_transfer:
                top_clearance = tls.aspirate_params_transfer.top_clearance_transfer
            if top_clearance is None:
                top_clearance = self.well_top_clearance.aspirate
        if bottom_clearance is None:
            if tls.aspirate_params_transfer:
                bottom_clearance = tls.aspirate_params_transfer.bottom_clearance_transfer
            if bottom_clearance is None:
                bottom_clearance = self.well_bottom_clearance.aspirate
        if manual_liquid_volume_allowance is None:
            if tls.aspirate_params_transfer:
                manual_liquid_volume_allowance = tls.aspirate_params_transfer.manual_manufacture_tolerance_transfer
            if manual_liquid_volume_allowance is None:
                manual_liquid_volume_allowance = self.config.aspirate.manual_liquid_volume_allowance

        current_liquid_volume = well.liquid_volume.current_volume_min
        needed_liquid_volume = well.geometry.min_aspiratable_volume + volume
        if current_liquid_volume < needed_liquid_volume:
            msg = pretty.format(
                'aspirating too much from well={0} have={1:n} need={2:n}',
                well.get_name(), current_liquid_volume, needed_liquid_volume)
            warn(msg)

        self._pre_wet(well, volume, location, rate, pre_wet)
        location = self._adjust_location_to_liquid_top(
            location=location,
            aspirate_volume=volume,
            top_clearance=top_clearance,
            bottom_clearance=bottom_clearance,
            manual_liquid_volume_allowance=manual_liquid_volume_allowance)

        def call_super():
            super(EnhancedPipette, self).aspirate(volume=volume,
                                                  location=location,
                                                  rate=rate)

        self.use_self_while(call_super)

        self.pause_after_aspirate(ms_pause)

        # finish up todo: what if we're doing an air gap
        well.liquid_volume.aspirate(volume)
        if volume != 0:
            self.prev_aspirated_well = well
def run(protocol: protocol_api.ProtocolContext):
    tiprack = protocol.load_labware(TIPRACK_LOADNAME, TIPRACK_SLOT)
    pipette = protocol.load_instrument(
        PIPETTE_NAME, PIPETTE_MOUNT, tip_racks=[tiprack])

    test_labware = protocol.load_labware_from_definition(
        LABWARE_DEF,
        TEST_LABWARE_SLOT,
        LABWARE_LABEL,
    )

    num_cols = len(LABWARE_DEF.get('ordering', [[]]))
    num_rows = len(LABWARE_DEF.get('ordering', [[]])[0])
    total = num_cols * num_rows
    pipette.pick_up_tip()

    def set_speeds(rate):
        protocol.max_speeds.update({
            'X': (600 * rate),
            'Y': (400 * rate),
            'Z': (125 * rate),
            'A': (125 * rate),
        })

        speed_max = max(protocol.max_speeds.values())

        for instr in protocol.loaded_instruments.values():
            instr.default_speed = speed_max

    set_speeds(RATE)

    pipette.home()
    if(PIPETTE_NAME == 'p20_single_gen2' or PIPETTE_NAME == 'p300_single_gen2' or PIPETTE_NAME == 'p1000_single_gen2' or PIPETTE_NAME == 'p50_single' or PIPETTE_NAME == 'p10_single' or PIPETTE_NAME == 'p300_single' or PIPETTE_NAME == 'p1000_single'):
        if(total > 1):
            #testing with single channel
            well = test_labware.well('A1')
            all_4_edges = [
                [well._from_center_cartesian(x=-1, y=0, z=1), 'left'],
                [well._from_center_cartesian(x=1, y=0, z=1), 'right'],
                [well._from_center_cartesian(x=0, y=-1, z=1), 'front'],
                [well._from_center_cartesian(x=0, y=1, z=1), 'back']
            ]

            set_speeds(RATE)
            pipette.move_to(well.top())
            protocol.pause("If the position is accurate click 'resume.'")

            for edge_pos, edge_name in all_4_edges:
                set_speeds(RATE)
                edge_location = types.Location(point=edge_pos, labware=None)
                pipette.move_to(edge_location)
                protocol.pause("If the position is accurate click 'resume.'")

            #last well testing
            last_well = (num_cols) * (num_rows)
            well = test_labware.well(last_well-1)
            all_4_edges = [
                [well._from_center_cartesian(x=-1, y=0, z=1), 'left'],
                [well._from_center_cartesian(x=1, y=0, z=1), 'right'],
                [well._from_center_cartesian(x=0, y=-1, z=1), 'front'],
                [well._from_center_cartesian(x=0, y=1, z=1), 'back']
            ]
            for edge_pos, edge_name in all_4_edges:
                set_speeds(RATE)
                edge_location = types.Location(point=edge_pos, labware=None)
                pipette.move_to(edge_location)
                protocol.pause("If the position is accurate click 'resume.'")
            set_speeds(RATE)
            #test bottom of last well
            pipette.move_to(well.bottom())
            protocol.pause("If the position is accurate click 'resume.'")
            pipette.blow_out(well)
        else:
            #testing with single channel + 1 well labware
            well = test_labware.well('A1')
            all_4_edges = [
                [well._from_center_cartesian(x=-1, y=0, z=1), 'left'],
                [well._from_center_cartesian(x=1, y=0, z=1), 'right'],
                [well._from_center_cartesian(x=0, y=-1, z=1), 'front'],
                [well._from_center_cartesian(x=0, y=1, z=1), 'back']
            ]

            set_speeds(RATE)
            pipette.move_to(well.top())
            protocol.pause("If the position is accurate click 'resume.'")

            for edge_pos, edge_name in all_4_edges:
                set_speeds(RATE)
                edge_location = types.Location(point=edge_pos, labware=None)
                pipette.move_to(edge_location)
                protocol.pause("If the position is accurate click 'resume.'")

            #test bottom of first well
            well = test_labware.well('A1')
            pipette.move_to(well.bottom())
            protocol.pause("If the position is accurate click 'resume.'")
            pipette.blow_out(well)
    else:
        #testing for multichannel
        if(total == 96 or total == 384): #testing for 96 well plates and 384 first column
            #test first column
            well = test_labware.well('A1')
            all_4_edges = [
                [well._from_center_cartesian(x=-1, y=0, z=1), 'left'],
                [well._from_center_cartesian(x=1, y=0, z=1), 'right'],
                [well._from_center_cartesian(x=0, y=-1, z=1), 'front'],
                [well._from_center_cartesian(x=0, y=1, z=1), 'back']
            ]
            set_speeds(RATE)
            pipette.move_to(well.top())
            protocol.pause("If the position is accurate click 'resume.'")

            for edge_pos, edge_name in all_4_edges:
                set_speeds(RATE)
                edge_location = types.Location(point=edge_pos, labware=None)
                pipette.move_to(edge_location)
                protocol.pause("If the position is accurate click 'resume.'")

            #test last column
            if(total == 96):
                last_col = (num_cols * num_rows) - num_rows
                well = test_labware.well(last_col)
                all_4_edges = [
                    [well._from_center_cartesian(x=-1, y=0, z=1), 'left'],
                    [well._from_center_cartesian(x=1, y=0, z=1), 'right'],
                    [well._from_center_cartesian(x=0, y=-1, z=1), 'front'],
                    [well._from_center_cartesian(x=0, y=1, z=1), 'back']
                ]
                for edge_pos, edge_name in all_4_edges:
                    set_speeds(RATE)
                    edge_location = types.Location(point=edge_pos, labware=None)
                    pipette.move_to(edge_location)
                    protocol.pause("If the position is accurate click 'resume.'")
                set_speeds(RATE)
                #test bottom of last column
                pipette.move_to(well.bottom())
                protocol.pause("If the position is accurate click 'resume.'")
                pipette.blow_out(well)
            elif(total == 384):
                #testing for 384 well plates - need to hit well 369, last column
                well369 = (total) - (num_rows) + 1
                well = test_labware.well(well369)
                pipette.move_to(well.top())
                protocol.pause("If the position is accurate click 'resume.'")
                all_4_edges = [
                    [well._from_center_cartesian(x=-1, y=0, z=1), 'left'],
                    [well._from_center_cartesian(x=1, y=0, z=1), 'right'],
                    [well._from_center_cartesian(x=0, y=-1, z=1), 'front'],
                    [well._from_center_cartesian(x=0, y=1, z=1), 'back']
                ]
                for edge_pos, edge_name in all_4_edges:
                    set_speeds(RATE)
                    edge_location = types.Location(point=edge_pos, labware=None)
                    pipette.move_to(edge_location)
                    protocol.pause("If the position is accurate click 'resume.'")
                set_speeds(RATE)
                #test bottom of last column
                pipette.move_to(well.bottom())
                protocol.pause("If the position is accurate click 'resume.'")
                pipette.blow_out(well)
        elif(num_rows == 1 and total > 1 and LABWARE_DIMENSIONS >= 71.2):
            #for 1 row reservoirs - ex: 12 well reservoirs
            well = test_labware.well('A1')
            all_4_edges = [
                [well._from_center_cartesian(x=-1, y=1, z=1), 'left'],
                [well._from_center_cartesian(x=1, y=1, z=1), 'right'],
                [well._from_center_cartesian(x=0, y=0.75, z=1), 'front'],
                [well._from_center_cartesian(x=0, y=1, z=1), 'back']
            ]
            set_speeds(RATE)
            pipette.move_to(well.top())
            protocol.pause("If the position is accurate click 'resume.'")

            for edge_pos, edge_name in all_4_edges:
                set_speeds(RATE)
                edge_location = types.Location(point=edge_pos, labware=None)
                pipette.move_to(edge_location)
                protocol.pause("If the position is accurate click 'resume.'")
            #test last well
            well = test_labware.well(-1)
            all_4_edges = [
                [well._from_center_cartesian(x=-1, y=1, z=1), 'left'],
                [well._from_center_cartesian(x=1, y=1, z=1), 'right'],
                [well._from_center_cartesian(x=0, y=0.75, z=1), 'front'],
                [well._from_center_cartesian(x=0, y=1, z=1), 'back']
            ]
            set_speeds(RATE)

            for edge_pos, edge_name in all_4_edges:
                set_speeds(RATE)
                edge_location = types.Location(point=edge_pos, labware=None)
                pipette.move_to(edge_location)
                protocol.pause("If the position is accurate click 'resume.'")
                #test bottom of first well
            pipette.move_to(well.bottom())
            protocol.pause("If the position is accurate click 'resume.'")
            pipette.blow_out(well)


        elif(total == 1 and LABWARE_DIMENSIONS >= 71.2 ):
            #for 1 well reservoirs
            well = test_labware.well('A1')
            all_4_edges = [
                [well._from_center_cartesian(x=-1, y=1, z=1), 'left'],
                [well._from_center_cartesian(x=1, y=1, z=1), 'right'],
                [well._from_center_cartesian(x=0, y=0.75, z=1), 'front'],
                [well._from_center_cartesian(x=0, y=1, z=1), 'back']
            ]
            set_speeds(RATE)
            pipette.move_to(well.top())
            protocol.pause("If the position is accurate click 'resume.'")

            for edge_pos, edge_name in all_4_edges:
                set_speeds(RATE)
                edge_location = types.Location(point=edge_pos, labware=None)
                pipette.move_to(edge_location)
                protocol.pause("If the position is accurate click 'resume.'")
                #test bottom of first well
            pipette.move_to(well.bottom())
            protocol.pause("If the position is accurate click 'resume.'")
            pipette.blow_out(well)

        else:
            #for incompatible labwares
            protocol.pause("labware is incompatible to calibrate with a multichannel pipette")




    set_speeds(1.0)
    pipette.return_tip()
Example #11
0
    def dispense(self,
                 volume: float = None,
                 location: Union[types.Location, Well] = None,
                 rate: float = 1.0) -> PairedInstrumentContext:
        """
        Dispense a volume of liquid (in microliters/uL) using both pipettes
        into the specified location.  You must pass in the location you
        wish the primary pipette to move to.

        For example, if your primary pipette is the left pipette and you
        wish for it to move to well ``A1`` of a 96 SBS format plate you should
        pass in well ``A1`` of this plate. The right pipette will automatically
        dispense into well ``A5`` of this plate.

        If only a volume is passed, the pipette will dispense from its current
        position. If only a location is passed (as in
        ``instr.dispense(location=wellplate['A1'])``), all of the liquid
        aspirated into both pipettes will be dispensed (this volume is
        accessible through :py:attr:`current_volume`).

        :param volume: The volume of liquid to dispense, in microliters. If not
                       specified, defaults to :py:attr:`current_volume`.
        :type volume: int or float

        :param location: Where to dispense into. If `location` is a
                         :py:class:`.Well`, the robot will dispense into
                         :py:obj:`well_bottom_clearance.dispense` mm
                         above the bottom of the well. If `location` is a
                         :py:class:`.Location` (i.e. the result of
                         :py:meth:`.Well.top` or :py:meth:`.Well.bottom`), the
                         robot will dispense into the exact specified location.
                         If unspecified, the robot will dispense into the
                         current position.
        :param rate: The relative plunger speed for this dispense. During
                     this dispense, the speed of the plunger will be
                     `rate` * :py:attr:`dispense_speed`. If not specified,
                     defaults to 1.0 (speed will not be modified).
        :type rate: float

        :returns: This instance.

        .. note::

            If ``dispense`` is called with a single argument, it will not try
            to guess whether the argument is a volume or location - it is
            required to be a volume. If you want to call ``dispense`` with only
            a location, specify it as a keyword argument:
            ``instr.dispense(location=wellplate['A1'])``

        """
        self._log.debug("dispense {} from {} at {}".format(
            volume, location if location else 'current position', rate))
        if isinstance(location, Well):
            if 'fixedTrash' in quirks_from_any_parent(location):
                loc = location.top()
            else:
                point, well = location.bottom()
                loc = types.Location(
                    point +
                    types.Point(0, 0, self.well_bottom_clearance.dispense),
                    well)
        elif location is not None and not isinstance(location, types.Location):
            raise TypeError(
                'location should be a Well or Location, but it is {}'.format(
                    location))
        else:
            loc = location

        if not volume:
            c_vol = self._get_minimum_current_volume(self._instruments)
        else:
            c_vol = volume

        instruments = list(self._instruments.values())
        primary_loc, dispense_func =\
            self.paired_instrument_obj.dispense(volume, loc, rate)

        if isinstance(primary_loc.labware, Well):
            labware = primary_loc.labware.parent
            well = primary_loc.labware
            locations = [
                primary_loc,
                self._get_secondary_target(labware, well)
            ]
        else:
            locations = [primary_loc]
        cmds.publish_paired(self.broker, cmds.paired_dispense, 'before', None,
                            instruments, c_vol, locations, rate)
        dispense_func()
        cmds.publish_paired(self.broker, cmds.paired_dispense, 'after', self,
                            instruments, c_vol, locations, rate)
        return self