Opentrons: bug: from_center_cartesian needs more guidance to be used in complex commands

Created on 1 Apr 2021  路  6Comments  路  Source: Opentrons/opentrons

Overview

The distribute() and transfer() commands (likely all of them) will dispense at slot 10 instead of the intended location if the well is using from_center_cartesian incorrectly (without warning). This will not be recorded in the run log either.

This looks like and is likely related to bug 5106 but unlike that bug which simply will ignore invalid arguments, this one will cause the OT-2 to move somewhere randomly.

Steps to reproduce

This protocol both simulates and appears in the run log as if nothing went wrong, but moves to the wrong location without warning.

from opentrons import protocol_api, types

metadata = {'apiLevel': '2.8'}

def run(protocol: protocol_api.ProtocolContext):
    tr2 = protocol.load_labware('opentrons_96_tiprack_20ul', '1')
    p20 = protocol.load_instrument('p20_single_gen2', 'left', tip_racks=[tr2])
    reservoir = protocol.load_labware('nest_12_reservoir_15ml', location='2')
    well = protocol.load_labware('nest_96_wellplate_100ul_pcr_full_skirt', location='4')
    p20.distribute(10, reservoir['A1'], [well['A1'].center().move(well['A1'].from_center_cartesian(x=0.1, y=0.1, z=0.1))])

What happens on step 4

Image from iOS (19)

Image from iOS (20)

Expected behavior

This should raise either TypeError, which is what Python normally raises when unexpected kwargs are provided (since it is passing a location type value as opposed to a well) . Alternatively, it should follow the behavior of #5106 and ignore this argument.

Ideal behavior

The ability for complex commands is to throw an error that this will occur.

Available workaround

Use the correct from_center_cartesian formatting and define the destinations like so:

center_cart = types.Location(well['A1'].from_center_cartesian(0, 0, -1), well["A1"])
from opentrons import protocol_api, types

metadata = {'apiLevel': '2.8'}

def run(protocol: protocol_api.ProtocolContext):
    #tr1 = protocol.load_labware('opentrons_96_tiprack_300ul', '3')
    tr2 = protocol.load_labware('opentrons_96_tiprack_20ul', '1')
    #p300_multi = protocol.load_instrument('p300_multi_gen2', 'right', tip_racks=[tr1])
    p20 = protocol.load_instrument('p20_single_gen2', 'left', tip_racks=[tr2])
    reservoir = protocol.load_labware('nest_12_reservoir_15ml', location='2')
    well = protocol.load_labware('nest_96_wellplate_100ul_pcr_full_skirt', location='4')
    center_cart = types.Location(well['A1'].from_center_cartesian(0, 0, -1), well["A1"])
    p20.distribute(10, reservoir['A1'], [center_cart])

鈹咺ssue is synchronized with this Wrike Task by Unito

api bug cpx docs papi v2

All 6 comments

Oof. Yeah.

This should raise either TypeError, which is what Python normally raises when unexpected kwargs are provided (since it is passing a location type value as opposed to a well). Alternatively, it should follow the behavior of #5106 and ignore this argument.

Not exactly.

labware['A1']
# opentrons.protocol_api.labware.Well

labware['A1'].from_center_cartesian(x=0, y=0, z=0)
# opentrons.types.Point
# containing absolute deck coordinates of the center of the well

labware['A1'].top()
# opentrons.types.Location
# top of the well

labware['A1'].center()
# opentrons.types.Location
# center of the well

labware['A1'].center().move(opentrons.types.Point(1, 2, 3))
# opentrons.types.Location
# center of the well plus 1 mm right, 2 mm back, 3 mm up

labware['A1'].center().move(from_center_cartesian(x=0, y=0, z=0))
# opentrons.types.Location
# center of the well plus an offset equal to the absolute deck coordinates of the center of the well
# (probably not the value you expected)

p.move_to(labware['A1'].center().move(from_center_cartesian(x=0.1, y=0.1, z=0.1)))
# valid
# but probably doesn't move where you expected

So, there's no type error here.

  • Location.from_center_cartesian() returns a Point, as intended.
  • Location.move() accepts a Point() and returns another Location, as intended.
  • InstrumentContext.move_to(), .distribute(), etc. will all accept a Location, as intended.

The confusing interaction is that Location.from_center_cartesian() returns absolute deck coordinates, whereas Location.move() interprets them as a relative offset.

This is why the pipette goes to a "random" spot on the deck. You're effectively telling the robot to add an offset of ~200 mm to A1. (~200 mm is the absolute deck coordinate of A1, plus the small x=0.1, y=0.1, z=0.1 offset.)

In my opinion, the underlying problem is that from_center_cartesian() is inherently difficult to use correctly. It's not only that we need to explain it better and add more parameter validation鈥攊t's that it does too much, and much of what it does is surprising.

It does all of this:

  1. It points to the center of a well, like well.center().
  2. It lets you apply a z-offset from that center. well.center() can't do this because it doesn't have a z parameter, even though well.top() and well.bottom() do.
  3. It lets you apply an x-offset and a y-offset. Neither well.top(), nor well.bottom(), nor well.center() have a built-in way of doing this鈥攜ou have to do something like well.top().move(Point(1, 2, 3)).
  4. It interprets those offsets relative to the dimensions of the well, instead of as millimeters. For example, well.from_center_cartesian(x=0,y=0,z=0.5) isn't a z-offset of 0.5 mm鈥攊t's 50% between the center and the top of the well. Neither well.top(), well.bottom(), nor well.center() do this鈥攚ith them, you have to do math yourself with well.diameter or well.depth.
  5. It returns the final position as absolute deck coordinates. As far as I know, nothing else in our API officially does this.

We should:

  1. Figure out: when people reach for from_center_cartesian(), which of those things do they actually want? What are they trying to do?
  2. Provide real ways to do those things, or document them better if they already exist.
  3. Deprecate from_center_cartesian(), pointing to the real ways instead.
  4. Someday remove from_center_cartesian().

In from_center_cartesian()'s defense, we never meant it to be part of our public API. But people relied on it somehow, so we were forced to retain it. https://github.com/Opentrons/opentrons/pull/6666#discussion_r498801151

Hi!

First, @SyntaxColoring thank you for your thoughtful follow up to this bug report. I have put in what I think would be good to do moving forward.

Part one, why do people want to use "from_center_cartesian"

from_center_cartesian() has value for people who want to pipette opposite of where the beads of the magnetic module are located. Just to avoid disturbing them or scraping them with the pipette.

Part two, how to make this more clear within the API documentation

I think we should put it into the API Reference Page
because it doesn't cleanly fit other locations (to me) for the following reasons.

  1. Labware is potentially too early because it is reviewed before the customer interacts with aspiration/dispense commands
  2. Building Block Commands and Complex Commands would have difficulty fitting because the other manipulating position commands aren't explicitly given this treatment.

What would these examples look like:

Block commands

from_center cartesian when used with block commands needs to have the location established first and then it can be passed into the block command as the location the block command is done in and will be treated as a location variable.

    center_cart = types.Location(well['A1'].from_center_cartesian(0, 0, -1), well["A1"])
    pipette.aspirate(10, center_cart)

Complex commands

The Distribute and other complex commands will reject a list of location parameters so it will require a for loop so that it treats each from_center_cartesian location as individually passed locations like so

pipette.distribute(
    [5, 10, 5],
    reservoir['A1'],
    [types.Location(well[well_name].from_center_cartesian(x = 0.8, y = 0.8, z= 0.1), well[well_name]) for well_name in ['B1', 'B2', 'B3']])

from_center_cartesian() has value for people who want to pipette opposite of where the beads of the magnetic module are located. Just to avoid disturbing them or scraping them with the pipette.

So, is it fair to say that among these 5 features (from https://github.com/Opentrons/opentrons/issues/7579#issuecomment-812756848)...

It does all of this:

  1. It points to the center of a well, like well.center().
  2. It lets you apply a z-offset from that center. well.center() can't do this because it doesn't have a z parameter, even though well.top() and well.bottom() do.
  3. It lets you apply an x-offset and a y-offset. Neither well.top(), nor well.bottom(), nor well.center() have a built-in way of doing this鈥攜ou have to do something like well.top().move(Point(1, 2, 3)).
  4. It interprets those offsets relative to the dimensions of the well, instead of as millimeters. For example, well.from_center_cartesian(x=0,y=0,z=0.5) isn't a z-offset of 0.5 mm鈥攊t's 50% between the center and the top of the well. Neither well.top(), well.bottom(), nor well.center() do this鈥攚ith them, you have to do math yourself with well.diameter or well.depth.
  5. It returns the final position as absolute deck coordinates. As far as I know, nothing else in our API officially does this.

...the value of from_center_cartesian() is specifically in feature (3)? For our purposes, we don't care about (1), (2), (4), or (5)?

If that's the case, instead of from_center_cartesian()...

location = types.Location(well_plate['A1'].from_center_cartesian(1, 0, 0), well_plate["A1"])
pipette.aspirate(10, location)

...could one use .move()?

location = well_plate['A1'].bottom().move(3, 0, 1)
pipette.aspirate(10, location)

Thanks @SyntaxColoring this seems like a helpful way to handle from_center_cartesian given the usability of .move() as a method to navigate within labware and directing customers instead to .move()

Was this page helpful?
0 / 5 - 0 ratings