Skip to content

Expose Well.bottom() & Well.top() #142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

BioCam
Copy link
Contributor

@BioCam BioCam commented May 25, 2024

Hi everyone,

Let's expose some of that Opentrons wisdom to PLR:

In this PR I added two small methods to the Well class in resources/well.py:

  1. Well.bottom(z_offset=0.0) ... convenient method to compute the absolute Coordinate of the 2D central bottom of a well, with the added functionality that a z_offset can directly be declared.
  2. Well.top(z_offset=0.0) ... similar as above just for the 2D central top of a well.

Why

With Well.get_absolute_location() already in place there is a valid question as to why generate these two methods.
The main reason is convenience and clarity to facilitate plate definition creation.

Currently, when defining labware there is no PLR probe_z_height_using_channel function (which I created in #69) that works with standard plates and tubes because probe_z_height_using_channel detects items using capacitative liquid level detection but cannot detect plates due to their material being highly electrically resistant.

As a consequence, the dimensions of a plate have to be manually evaluated. In particular the depth of a well represents a small but important challenge: it is arguably one of the most important definitions to get right to avoid crashes and minimise dead volume.

It is therefore important that the machine used enables moving a tip quickly to the bottom of a well and to the top of a well, back and forth, with the ability to efficiently add a z_offset to each Coordinate.
(A laminated piece of paper is very effective at testing whether Well.top() is indeed the top of the well, and when moving the tip to Well.bottom(), the tip can be slightly moved by hand, sensing slight resistance to the move, i.e. the tip hasn't crashed.)

Example Use Case

A step-by-step guide to conveniently and transparently use these little methods:

# Define plate carrier & assign to deck
plt_carrier_1 = PLT_CAR_L5AC_A00(name='plt_carrier_1')
lh.deck.assign_child_resource(plt_carrier_1, rails=1)

# Define & assign plate to carrier site
plt_carrier_1[0] = ThermoScientific_96_1200ul_Rd_1 = ThermoScientific_96_1200ul_Rd(name="ThermoScientific_96_1200ul_Rd")

# Pick up tip (any way you want to, there are many options to automate tip selection)

# Safety first :)
await lh.backend.move_all_channels_in_z_safety() # safe z height

y_pos_start = 10    # safe y positions (for an 8-channel STAR system, be careful with 12- & 16-channel systems)
for channel_idx in reversed(range(0,8)):
    print(f"channel {channel_idx} -> {y_pos_start}")
    await lh.backend.move_channel_y(y=y_pos_start, channel=channel_idx)
    y_pos_start += 9
# channel 7 -> 10
# channel 6 -> 19
# channel 5 -> 28
# channel 4 -> 37
# channel 3 -> 46
# channel 2 -> 55
# channel 1 -> 64
# channel 0 -> 73

# Move tip to plate well for definition testing
test_well_bottom_coordinate = ThermoScientific_96_1200ul_Rd_1['H1'][0].bottom()
# Coordinate(x=117.75, y=82.95, z=186.35)

test_well_top_coordinate = ThermoScientific_96_1200ul_Rd_1['H1'][0].top()
# Coordinate(x=117.75, y=82.95, z=206.85)

# note channel_0 (most backwards channel) can currently move in y to any well without crashing into another channel
# do not use any other channel, because it will crash :)

await lh.backend.move_channel_x(x=test_well_bottom_coordinate.x, channel=0)
await lh.backend.move_channel_y(y=test_well_bottom_coordinate.y, channel=0)


# Probe correctness of the top of the well: place laminated piece of paper on top of well and
# move channel_0 incrementally until paper cannot be moved without some resistance

# await lh.backend.move_channel_z(z=test_well_top_coordinate.z+2, channel=0)
# await lh.backend.move_channel_z(z=test_well_top_coordinate.z+1, channel=0)
# await lh.backend.move_channel_z(z=test_well_top_coordinate.z, channel=0)
...
await lh.backend.move_channel_z(z=test_well_top_coordinate.z-3.2, channel=0)

# => you found that the top of the well is actually 3.2 mm below where PLR thinks it is


# Probe correctness of the bottom of the well: move tip a couple of mm above the bottom
# and incrementally move tip down in z until manual tip "jiggling" encounters some resistance

# await lh.backend.move_channel_z(z=test_well_bottom_coordinate.z+2, channel=0)
# await lh.backend.move_channel_z(z=test_well_bottom_coordinate.z+1, channel=0)
await lh.backend.move_channel_z(z=test_well_bottom_coordinate.z, channel=0)

# => you found that the bottom of the well is correctly defined

# Calculate the true depth of the well
(test_well_top_coordinate.z-3.2) - test_well_bottom_coordinate.z

# => update Plate definition based on your labware testing results 
# & submit a pull request to share your findings with the community

I hope this helps PLR users to create more robust labware definitions.

@rickwierenga
Copy link
Member

I think it would be better if bottom and top and other "anchors" used relative locations, so that they can easily be used in offsets for lh operations. The actual bottom is then an explicit well.get_absolute_location() + well.bottom().

@rickwierenga
Copy link
Member

I love the extensive documentation and motivation (as always) and it's definitely a useful add, whatever the implementation is!

@BioCam
Copy link
Contributor Author

BioCam commented May 26, 2024

I think it would be better if bottom and top and other "anchors" used relative locations, so that they can easily be used in offsets for lh operations. The actual bottom is then an explicit well.get_absolute_location() + well.bottom().

Let's discuss :)

I think the question to answer is "what is well.bottom() and well.top() likely going to be used for?"
Based on the answer to that question we should choose the most appropriate implementation.

I have found these two methods very useful for labware definition investigation, which is why I showed them as examples in this PR.
The reason I found the current implementation so useful for this is because I don't have to write out well.get_absolute_location()+well.center(True,True,False)+well.get_size_z() every time, and keep a mental image of this being the top of the well.
Instead I can just say well.top().

I could imagine though that an "anchor" rather than absolute location approach might be useful for other applications?

Like you said maybe for aspirate, e.g.:

await lh.aspirate(
    plate['H1'], 
    vols=[0],
    lld_mode=[lh.backend.LLDMode(0),
    offsets=Coordinate(0,0,well.top().z),
    use_channels=[0],
    liquid_class_dict=liquid_class_x)

But in that situation I would say it is just as easy to call the well_depth, which already exists, instead:

await lh.aspirate(
    plate['H1'], 
    vols=[0],
    lld_mode=[lh.backend.LLDMode(0),
    offsets=Coordinate(0,0,well.get_size_z()),
    use_channels=[0],
    liquid_class_dict=liquid_class_x)

Meaning, in this specific situation I don't think the "anchor" implementation yields a benefit to current solutions.

@rickwierenga
Copy link
Member

Coordinate(0,0,well.top().z),

Agree that this is not optimal, and duplicated from well.get_size_z().

A second thought: the center is already a relative. If top is absolute, that might cause confusion.

I am also thinking about something like this: get_absolute_location(x="l", y="b", z="b") for left, back, bottom; or "c" center or, "r" right, "f" front, "t" top. Default to "c". and get_relative_location()

@BioCam
Copy link
Contributor Author

BioCam commented May 27, 2024

A second thought: the center is already a relative. If top is absolute, that might cause confusion.

I see what you mean: switching back and forth between anchors and absolute coordinates is indeed confusing.

I am also thinking about something like this: get_absolute_location(x="l", y="b", z="b") for left, back, bottom; or "c" center or, "r" right, "f" front, "t" top. Default to "c". and get_relative_location()

I love that idea!

Should we then just make the pure .bottom() and .top() into anchors,
and implement the enhancement for get_absolute_location(x="center", y="center", z="bottom").
That's the default, takes "b" or "bottom" etc?

@rickwierenga
Copy link
Member

That sounds good to me!

@rickwierenga rickwierenga mentioned this pull request May 28, 2024
@rickwierenga
Copy link
Member

what do you think about this: #147

@BioCam
Copy link
Contributor Author

BioCam commented May 28, 2024

Love it, let's merge #147 and close this PR (i.e. #142)

@rickwierenga
Copy link
Member

done! thanks for the work here & "asking" for this feature! :)

@BioCam
Copy link
Contributor Author

BioCam commented May 28, 2024

done! thanks for the work here & "asking" for this feature! :)

Haha no worries, I always think it's more productive to ask with a suggestion of the solution (if possible), makes things move faster 😄

Thank you for the fast replies and finding an even better solution!

@BioCam BioCam deleted the Implement-Well.bottom-&-Well.top branch June 5, 2024 19:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants