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.
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))])


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.
The ability for complex commands is to throw an error that this will occur.
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
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:
well.center().well.center() can't do this because it doesn't have a z parameter, even though well.top() and well.bottom() do.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)).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.We should:
from_center_cartesian(), which of those things do they actually want? What are they trying to do?from_center_cartesian(), pointing to the real ways instead.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.
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.
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.
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)
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:
- It points to the center of a well, like
well.center().- It lets you apply a z-offset from that center.
well.center()can't do this because it doesn't have azparameter, even thoughwell.top()andwell.bottom()do.- It lets you apply an x-offset and a y-offset. Neither
well.top(), norwell.bottom(), norwell.center()have a built-in way of doing this鈥攜ou have to do something likewell.top().move(Point(1, 2, 3)).- 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. Neitherwell.top(),well.bottom(), norwell.center()do this鈥攚ith them, you have to do math yourself withwell.diameterorwell.depth.- 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()