Skip to content
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

Build ztouch Probing Function #260

Merged
merged 16 commits into from
Nov 8, 2024

Conversation

BioCam
Copy link
Contributor

@BioCam BioCam commented Sep 24, 2024

Hi everyone,

In this PR I am integrating a way of probing the height of non-conductive materials, e.g. 96-well plates, and standard tubes, to enable their automated definition generation.

The Problem

In PR #69 I integrated the function probe_z_height_using_channel which uses Hamilton STAR(let) channels' capacitive Liquid Level Detection (cLLD) capability to detect the z-height of any conductive material.

Though very useful and highly precise, this function does not allow z-height probing of the resources/labware we have to most commonly generate definitions for, plates and tubes, because these resources are made of highly insulating/non-conductive plastic.

Task definition: We therefore urgently need a function that uses only the machine available (STAR(let)) to identify the z-height of these non-conductive materials.

PR Content

Here I ...

  1. introduce a new function called STAR.ztouch_probe_z_height_using_channel(channel_idx: int, tip_len: float): this function uses the same functionality that aspirate and dispense with lld_mode=4, i.e. z-touchoff, has. In other words, the channel is conducting a controlled "crash" of the tip, which triggers some form of force-sensor in the Z-drive.
    This enables the probing of non-conductive materials, including plates and tubes.
  2. Rename STAR.probe_z_height_using_channel() to STAR.clld_probe_z_height_using_channel(), to clearly distinguish these two probing functions,
  3. convert both functions to accept attributes in millimetre (mm) (and homogenise their attributes as much as possible to make it easier to switch between the two functions),
  4. convert both functions to use 0-based channel indexing (0 is the backmost channel from a user standing in front of the machine).

The current version of the new function:

class STAR(HamiltonLiquidHandler):
  ...
  async def ztouch_probe_z_height_using_channel(
    self,
    channel_idx: int, # 0-based indexing of channels!
    tip_len: float, # mm
    lowest_immers_pos: float = 100.0, # mm
    start_pos_search: float = 330.0, # mm
    channel_speed: float = 10.0, # mm/sec
    channel_acceleration: float = 800.0, # mm/sec**2
    channel_speed_upwards: float = 125.0, # mm
    detection_limiter_in_PWM: int = 1,
    push_down_force_in_PWM: int = 0,
    post_detection_dist: float = 2.0, # mm
    move_channels_to_save_pos_after: bool = False
    ) -> float:
    """ Probes the Z-height below the specified channel on a Hamilton STAR liquid handling machine
    using the channels 'z-touchoff' capabilities, i.e. a controlled triggering of the z-drive,
    aka a controlled 'crash'.
    ...
    """ 
Video Title

Next Steps

I have added one TODO to STAR.ztouch_probe_z_height_using_channel():
At the moment, users have to directly declare the total_tip_length to the tip_len argument.

This can easily be automated at runtime using:

measured_z_touch = await lh.backend.ztouch_probe_z_height_using_channel(
    channel_idx=0,
    tip_len=lh.head[0].get_tip().total_tip_length,
    channel_speed=6.0, start_pos_search=250.0,
    detection_limiter_in_PWM=0, push_down_force_in_PWM=0,
    move_channels_to_save_pos_after=False
)

i.e. by accessing the total_tip_length via the liquid_handler.

I would like to remove this attribute altogether and have the function retrieve the total_tip_length information by itself at the STAR level, rather than the liquid_handler level.

Notes

How accurate is STAR.ztouch_probe_z_height_using_channel()?

I performed an array of tests in which I ztouch_probed conductive materials and compared their measured z-height against the same positions clld_probed height:
I still need to write the complete analysis up, but in short: ztouch_probing deviates between 0 (minimum) - 320 (maximum) micrometers, in my hands.
This is in absolute terms.

Does this matter?

Whether this deviation matters depends on what you are trying to measure:
If you try to identify absolute locations, it likely matters, and I recommend using clld_probing in these cases - if possible, because the material is conductive (e.g. PlateCarrierSite mapping).

However, if you try to identify relative positions you can use ztouch_probing for measuring both/multiple locations of interest with low to no concern about this deviation:
e.g. all Containers require defining their material_z_thickness

see PR #183; example:
material_z_thickness_example

This attribute of Container is very difficult to actually measure (e.g. because calipers cannot enter the container, ...).
But using ztouch_probing one can identify the well_cavity_bottom, then remove the plate, and use ztouch_probing again to identify the height of the site below.
If the PlateCarrierSite has a pedestal, the difference between these two measurements is the material_z_height ... measured, and therefore automatable.

Since both measurements can be assumed to experience any absolute deviation in the same way, their difference holds true, regardless of absolute deviation between ztouch_probing and clld_probing :)

Does it damage the channel?

There might be some concern about damaging the channel when using controlled "crashes" to probe resources.
Nothing can guarantee no damage.
But as long as the force and detection attributes are not pushed to their extremes (i.e. detection_limiter_in_PWM & push_down_force_in_PWM), channel damage is deemed very unlikely.
I defaulted these values to 1 and 0 respectively, making the force-sensing extremely responsive and essentially generating no noticeable force, to decrease damaging risks in the odd case that anyone accidentally finds these functions and doesn't read this PR/the documentation pages (work in progress).

What tips can you use?

All standard tips, i.e. 10 ul, 50 ul, 300 ul, 1000 ul, work using ztouch_probing, and you are not limited to using their conductive version but might also use their transparent or needle/metal version.

That being said, there might be a slight change in accuracy depending on tip choice:
Lower-volume tips are thinner and can buckle under the ztouch_probing force generated which might give a lower z-height than real.
I therefore switched to using a teaching needle for probing (see image above of probing a PCR 96-well plate).

However, if one does not have needles/metal tips available and requires a quick measurement it is possible to successfully use standard tips, taking a mental note of the caveats discussed here.


Please let me know whether you have any suggestions on how to further improve these two functions.

I expect these updates to save us all time, and to lead to highly robust PLR Resource Library contributions.

Happy automation 🦾

@rickwierenga
Copy link
Member

I would like to remove this attribute altogether and have the function retrieve the total_tip_length information by itself at the STAR level, rather than the liquid_handler level.

This information depends on the tip that is currently mounted, which is only known by LiquidHandler. I tried to make STAR as stateless as possible. The tips used are passed to say STAR.aspirate by LiquidHandler.aspirate through the ops array when doing an aspiration.

In this case, a call is made directly to the backend where this information is not available. One way to do this is to go through LH, but that seems suboptimal because this functionality is not generally applicable. The alternative seems to be passing the parameter directly the backend (if we want to keep STAR with minimal state). We can add a convenience feature to LiquidHandler for getting the length of the currently mounted tips if helpful.

@BioCam
Copy link
Contributor Author

BioCam commented Sep 24, 2024

The alternative seems to be passing the parameter directly the backend (if we want to keep STAR with minimal state). We can add a convenience feature to LiquidHandler for getting the length of the currently mounted tips if helpful.

I am not sure. Do you think a convenience feature would be clearer than my example from above?:

This can easily be automated at runtime using:

measured_z_touch = await lh.backend.ztouch_probe_z_height_using_channel(
    channel_idx=0,
    tip_len=lh.head[0].get_tip().total_tip_length,
    channel_speed=6.0, start_pos_search=250.0,
    detection_limiter_in_PWM=0, push_down_force_in_PWM=0,
    move_channels_to_save_pos_after=False
)

i.e. by accessing the total_tip_length via the liquid_handler.

I guess lh.head[0].get_tip().total_tip_length is already taking this potential feature's role?

I understand the reason for statelessness in the STAR backend. Keeping the resource management system and the machine control system, i.e. machine backend, separate has definitely many advantages.
But it does seem like it limits the backend in this case, and makes it dependent on the liquid_handler.

I don't see a path around this either and we'll just have to hand the backend the tip_length.

...what I don't understand is how STAR.clld_probe_z_height_using_channel() appears to somehow circumvent this issue.
We don't tell the cLLD function what the tip_len is, and yet it works with all conductive tips....

@rickwierenga
Copy link
Member

i had to add

lowest_immers_pos=100,

because AssertionError: Lowest immersion position must be between 99.98 and 334.7 mm..

unfortunately it seems i have to update the channel firmware

STARFirmwareError: {'Pipetting channel 2': UnknownHamiltonError('Unknown command')}, P2ZHid0019er30

@rickwierenga
Copy link
Member

. Do you think a convenience feature would be clearer than my example from above?:

not necessarily, up to you

But it does seem like it limits the backend in this case, and makes it dependent on the liquid_handler.

Very much by design. LiquidHandler is the state manager.

...what I don't understand is how STAR.clld_probe_z_height_using_channel() appears to somehow circumvent this issue.
We don't tell the cLLD function what the tip_len is, and yet it works with all conductive tips....

I guess it knows which tip is mounted, and the length is defined with TT earlier?

(also, only tangentially related, when discarding tips to trash it will just move until it feels resistance, and then discard. but you don't get a reading from that)

@rickwierenga
Copy link
Member

ztouch_probe_z_height_using_channel should then also know this, but maybe we can't assume all the firmware commands are at the same level/written at the same time

@BioCam
Copy link
Contributor Author

BioCam commented Sep 25, 2024

unfortunately it seems i have to update the channel firmware

What is your return from?

await lh.backend.send_command(
    module="PX",
    command="RF" # request firmware
    )

ztouch_probe_z_height_using_channel should then also know this, but maybe we can't assume all the firmware commands are at the same level/written at the same time

I completely agree; I'm not sure how the firmware keeps memory of the tip but I think you are right that we cannot assume different firmware commands are written the same way.
In fact the clld_probing and ztouch_probing return the measured z_height in completely different formats:
clld_probing returns the z_height in dmm
and ztouch_probing does in increment ... I abstracted all this complexity in the functions I created because the user should not have to be bothered by this inconsistency.

Btw, what do people think about the name "ztouch_probing"? My alternative suggestion is "force_probing" but since I don't know the mechanism of action behind the force sensing I was hesitant to use it, and "ztouch_probing" is a reference to the lld_mode=4 / "z-touchoff" that inspired me to create this function.
But happy to hear what the community finds more intuitive.

@rickwierenga
Copy link
Member

'P1RFid0012rf4.0S f 2020-07-31'

I like ztouch_probing!

@jrast
Copy link
Contributor

jrast commented Sep 26, 2024

I guess it knows which tip is mounted, and the length is defined with TT earlier?

ztouch_probe_z_height_using_channel should then also know this, but maybe we can't assume all the firmware commands are at the same level/written at the same time

This seems to be discrepancy between the two commands which might or might not be intended. Basically the tip length would also be available through TT.

@jrast
Copy link
Contributor

jrast commented Sep 26, 2024

. Do you think a convenience feature would be clearer than my example from above?:

not necessarily, up to you

But it does seem like it limits the backend in this case, and makes it dependent on the liquid_handler.

Very much by design. LiquidHandler is the state manager.

...what I don't understand is how STAR.clld_probe_z_height_using_channel() appears to somehow circumvent this issue.
We don't tell the cLLD function what the tip_len is, and yet it works with all conductive tips....

I guess it knows which tip is mounted, and the length is defined with TT earlier?

(also, only tangentially related, when discarding tips to trash it will just move until it feels resistance, and then discard. but you don't get a reading from that)

Slightly off-topic: Having the LiquidHandler as state manager is probably the right way to go. At the same time the .deck on the Backend also handles state (of course it's also available at other levels) and the backend has easy access to this state. Maybe something similar would also be nice for the state of the liquid handler?

@BioCam
Copy link
Contributor Author

BioCam commented Sep 26, 2024

This seems to be discrepancy between the two commands which might or might not be intended. Basically the tip length would also be available through TT.

I'm not sure I understand what you mean, @jrast: C0TT, PLR-exposed as STAR.define_tip_needle(), is a definition command.

To my understanding, it does not enable "request tip_length on channel n", which is what we'd need to remove the attribute tip_len from STAR.ztouch_probe_z_height_using_channel(channel_idx: int, tip_len: float).

@BioCam
Copy link
Contributor Author

BioCam commented Sep 26, 2024

Slightly off-topic: Having the LiquidHandler as state manager is probably the right way to go. At the same time the .deck on the Backend also handles state (of course it's also available at other levels) and the backend has easy access to this state. Maybe something similar would also be nice for the state of the liquid handler?

This is an amazing idea!

It would enable a whole array of automated requests, and solve the TODO I placed into STAR.ztouch_probe_z_height_using_channel(channel_idx: int, tip_len: float).


To finish this PR:
With @rickwierenga currently not having the necessary 2022+ firmware version of module="PX", is there anybody else who could verify/peer review the changes/additions to PLR from this PR?

@jrast
Copy link
Contributor

jrast commented Sep 27, 2024

This seems to be discrepancy between the two commands which might or might not be intended. Basically the tip length would also be available through TT.

I'm not sure I understand what you mean, @jrast: C0TT, PLR-exposed as STAR.define_tip_needle(), is a definition command.

To my understanding, it does not enable "request tip_length on channel n", which is what we'd need to remove the attribute tip_len from STAR.ztouch_probe_z_height_using_channel(channel_idx: int, tip_len: float).

Sorry, this was not clear. I was talking about the Firmware which would have access to the tip length by looking at what's in the tip definition table written with the TT command. And indeed, the STAR firmware offers no command to query the tip definition table, at least I'm not aware of a command. Newer devices offer a command to read the table.

@BioCam
Copy link
Contributor Author

BioCam commented Sep 27, 2024

@jrast, I had a bit of an epiphany and invented a way for the STAR to measure the tip length on a specified channel, even though, as you confirmed, it is impossible to request/query that information from the firmware, and made it into a standalone function for us:

async def request_tip_len_on_channel(
    self,
    channel_idx: int, # 0-based indexing of channels!
    ) -> float:
    """
    Measures the length of the tip attached to the specified pipetting channel.

    Checks if a tip is present on the given channel. If present, moves all channels
    to THE safe Z position, 334.3 mm, measures the tip bottom Z-coordinate, and calculates 
    the total tip length. Supports tips of lengths 50.4 mm, 59.9 mm, and 95.1 mm.
    Raises an error if the tip length is unsupported or if no tip is present.

    Parameters:
        channel_idx (int): Index of the pipetting channel (0-based).

    Returns:
        float: The measured tip length in millimeters.

    Raises:
        ValueError: If no tip is present on the channel or if the tip length is
          unsupported. 
    """

    # Check there is a tip on the channel
    all_channel_occupancy = await self.request_tip_presence()

    if all_channel_occupancy[channel_idx]:
      # Level all channels
      await self.move_all_channels_in_z_safety()
      known_top_position_channel_head = 334.3 # mm
      fitting_depth_of_all_standard_channel_tips = 8 # mm
      unknown_offset_for_all_tips = 0.4 # mm

      # Request z-coordinate of channel+tip bottom
      tip_bottom_z_coordinate = await self.request_z_pos_channel_n(
        pipetting_channel_index=channel_idx
        )

      total_tip_len = round(known_top_position_channel_head - (
        tip_bottom_z_coordinate - fitting_depth_of_all_standard_channel_tips - \
        unknown_offset_for_all_tips
      ),1)

      if total_tip_len in [50.4, 59.9, 95.1]: # 50ul, 300ul, 1000ul
        return total_tip_len
      else:
        raise ValueError(f"Tip of length {total_tip_len} not yet supported")

    else:
      raise ValueError(f"No tip present on channel {channel_idx}")

Does anyone have an idea where the unknown_offset_for_all_tips = 0.4 # mm comes from?

I identified them empirically here with the following measurements:

- 50ul: 334.3 - 292.3 = 42.0 + 8 = 50.0 mm -> 50.4 mm (actual tip_50ul_len)
- 300ul: 334.3 - 282.8 = 51.5 + 8 = 59.5 mm -> 59.9 mm (actual tip_300ul_len)
- 1000ul: 334.3 - 247.6 = 86.7 + 8 = 94.7 mm -> 95.1 mm (actual tip_1000ul_len)

They are also part of the tip definitions.
My best guess is that they represent the curvature between tip shaft and tip_fitting_depth compartment?

@rickwierenga
Copy link
Member

Maybe something similar would also be nice for the state of the liquid handler?

Where should this go though? It is fundamentally a property of LH. deck is externally managed (in the pylabrobot.resources package) and so both the frontend and backend can point to it.

request_tip_len_on_channel

I love this!

if total_tip_len in [50.4, 59.9, 95.1]: # 50ul, 300ul, 1000ul
        return total_tip_len
      else:
        raise ValueError(f"Tip of length {total_tip_len} not yet supported")

why not just return total_tip_len?

@BioCam
Copy link
Contributor Author

BioCam commented Sep 27, 2024

I love this!

There's almost always a code solution :)

Imagine the benefit to Hamilton:
Instead of having to create a new firmware command to do the same task - which not only requires development time and money but would also require accessing every STAR(let) ever sold to be manually firmware updated by a Hamilton engineer (because in contrast to Opentrons machines, Hamilton machines are not online, i.e. their firmware cannot be remotely upgraded [to my knowledge]) -
...every STAR(let) user in the world can use this functionality as soon as this PR is merged, even if they have an older model/firmware :)

Caveat: to get this information the channel_head will move to the safe z-height=334.3 mm (pretty much the opposite of a danger to the machine, but creates a little bit of a bouncing movement)

why not just return total_tip_len?

Because we don't know any potential offset for other tips.

I could only measure the offset for 50ul, 300ul, and 1_000ul Hamilton CO-REII tips. (standard teaching needles are simply 300ul tips in terms of their definition)
Slim tips, wide-bore tips and 10ul have not been tested. 10ul tips have a good chance of having the same offset.
But since we don't know the real source of this 0.4 mm offset yet, we cannot assume that it will be the same for the remaining tips.

This way people who do want to use these tips have the ability to simply add the corresponding tip_len - after empirical validation - to this list.

(Though I would not recommend trying slim fit tips from a design perspective because of the above mentioned buckling issue, and wide-bore tips are unlikely to reach the very bottom of narrow cavities, e.g. PCR_wellplate well bottoms)

@jrast
Copy link
Contributor

jrast commented Oct 3, 2024

So to catch up on some of the questions:

Does anyone have an idea where the unknown_offset_for_all_tips = 0.4 # mm comes from?

I can't say for sure on the STAR platform. I know of some internal position offsets / shifts on the Vantage / STAR V platform which are related to the CORE-I / CORE-II (tip coupling mechanisms) change.

On Vantage there should also be a easier way to get the tip length of the mounted tip, when I find the code snipped I can provide some guidance here.

... (because in contrast to Opentrons machines, Hamilton machines are not online, i.e. their firmware cannot be remotely upgraded [to my knowledge]) -

I can confirm this: Hamilton has currently no means to update (or access) the device remotly.

Maybe something similar would also be nice for the state of the liquid handler?

Where should this go though? It is fundamentally a property of LH. deck is externally managed (in the pylabrobot.resources package) and so both the frontend and backend can point to it.

Good question... And if the state of the LH is exposed, the next question would be "what is considered part of the state"?

@rickwierenga rickwierenga force-pushed the ztouch-probing-feature branch from ec230b5 to ca55114 Compare October 30, 2024 23:11
@rickwierenga
Copy link
Member

i don't think it works with 50uL tips LOL (as you pointed out in 69, these tips are good for safety precisely because they are soft)

IMG_6779

with 10uL tips it works though! I see a tiny bit of bending which makes me question the accuracy

@rickwierenga
Copy link
Member

how would we quantify accuracy? can we use the PLR model as a ground truth? (precision is easy to measure)

@BioCam
Copy link
Contributor Author

BioCam commented Oct 31, 2024

i don't think it works with 50uL tips LOL (as you pointed out in 69, these tips are good for safety precisely because they are soft)

IMG_6779

with 10uL tips it works though! I see a tiny bit of bending which makes me question the accuracy

ohh wow, what detection_limiter_in_PWM and push_down_force_in_PWM settings are you using?

I would say though, yes, 50ul tips are the softest and buckle quite easily (even though I haven't seen them being crushed likes this before.

how would we quantify accuracy? can we use the PLR model as a ground truth? (precision is easy to measure)

The question here is Where does the PLR model come from?

Some resource dimensions are not realistically measurable by hand, e.g. the material_z_thickness of a well.
This new ztouch_probing finally gives us an automatable way of measuring this value.

This means we might not be able to compare the ztouch_probing measurements to a PLR model because there is no PLR model yet, and the point of ztouch_probing is to generate a PLR model.

-> My alternative: use the metal teaching needle that all machines already have:
Definition-wise it is just a 300ul / "ST" tip.
But it's metal (I believe stainless steel) and won't buckle.
Then take 5+ measurements of each position you want to z-measure and let the data guide you towards the true value.

@BioCam
Copy link
Contributor Author

BioCam commented Oct 31, 2024

One update I was thinking of after our last conversation that I would like to implement before we merge:

adding an "RF" (request firmware) command at the beginning of the ztouch_probing function.

Raise an error if firmware <2022, explaining that their machines PX module has to be upgraded to enable this functionality.
(The direct firmware error is not useful/informative enough, and people might think PLR has an issue when in reality their machine "just" requires updating)

@rickwierenga
Copy link
Member

rickwierenga commented Nov 7, 2024

ohh wow, what detection_limiter_in_PWM and push_down_force_in_PWM settings are you using?

defaults

we get tips from a variety of sources, and perhaps these are just very very old 🤷‍♂️

My alternative: use the metal teaching needle that all machines already have:

playing with fire (steel)


anything to do before merging?

@rickwierenga rickwierenga force-pushed the ztouch-probing-feature branch from ab37585 to cfdd227 Compare November 8, 2024 22:22
@rickwierenga rickwierenga force-pushed the ztouch-probing-feature branch from 4f75a03 to 44c2617 Compare November 8, 2024 22:28
@rickwierenga rickwierenga merged commit 7ca03b7 into PyLabRobot:main Nov 8, 2024
7 checks passed
@rickwierenga
Copy link
Member

Really cool change that will make calibrating labware a lot easier!

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.

3 participants