Volume tracker on mix cycles

I’m trying to run this:


    for i in range(0,8):
        reagent_source.set_liquid(liquid=Liquid.SERUM, volume=55, well=i)

    await lh.pick_up_tips(tip_rack_200s[0,1,2,3,4,5,6,7], use_channels=[0,1,2,3,4,5,6,7])
    for _ in range(10):
        await lh.aspirate(reagent_source[0,1,2,3,4,5,6,7], vols=45)
        await lh.dispense(reagent_source[0,1,2,3,4,5,6,7], vols=45)

However, on the 2nd aspirate cycle, it pylabrobot.resources.errors.TooLittleLiquidError: Tracker only has 10.0uL

When I step through the sequence of actions - it looks like the tip’s volume tracker isn’t committed during the aspirate step. So the dispense isn’t getting a liquid type and volume set, which means no volume is added to the well’s tracker.

I’m still a bit new to PLR’s code base, so not sure if this is me or a bug. Anyone else tried this?

1 Like

looking into it

1 Like

Where is reagent_source.set_liquid defined? I can only find set_liquids in the latest version, or set_well_liquids for Plate. My hope is you’re running a slightly outdated version so you can just update.

Added a small test to make sure this works:

for i in range(8):
  self.plate.get_item(i).set_liquids([(Liquid.SERUM, 55)])

await self.lh.pick_up_tips(self.tip_rack[0:8])
initial_liquids = [self.plate.get_item(i).tracker.liquids for i in range(8)]
for _ in range(10):
  await self.lh.aspirate(self.plate[0:8], vols=45)
  await self.lh.dispense(self.plate[0:8], vols=45)
liquids_now = [self.plate.get_item(i).tracker.liquids for i in range(8)]
self.assertEqual(liquids_now, initial_liquids)

(self.plate = reagent_source, using an existing part of the unit test.)

Sorry, should have mentioned that. I added a small method to the Plate class:

  def set_liquid(
      self,
      liquid: Liquid,
      volume: float,
      well: int
  ) -> None:
    _well = self.get_well(well)
    _well.tracker.set_liquids([(liquid, volume)])

That was another thing I meant to ask about - I wasn’t able to set different liquids into different wells of a plate. I’ll start a separate thread on that…

I also added this to the aspirate/dispense methods:

    # commit or rollback the state trackers
    for channel, op, success in zip(use_channels, dispenses, successes):
      if does_volume_tracking():
        if not op.resource.tracker.is_disabled:
          (op.resource.tracker.commit if success else op.resource.tracker.rollback)()
          (op.tip.tracker.commit if success else op.tip.tracker.rollback)() #added by RK
        (self.head[channel].commit if success else self.head[channel].rollback)()

That seems to result in the well and tip trackers volumes to be correct after each step (as read out by:

   for op in dispenses:
      print(op.resource.name)
      print(op.resource.tracker.serialize())
      print(op.tip.tracker.serialize())

could you please confirm you’re running the latest version?

Please!

this is supposed to be self.head[channel].get_tip().tracker (a VolumeTracker) instead of self.head[channel].commit. Fixed recently-ish with fix LH.dispense volume tracker issue · PyLabRobot/pylabrobot@c655ceb · GitHub.

I’m a bit confused because your code above seems to run fine in the unit tests. I can’t reproduce this issue on plr:main.

Bumping this rather than a new thread as I think I may be running into something related.

I’m trying to do an aspirate, dispense, mix cycle. It happens to be from/to two quadrants of a 384 plate and at low volume. My code looks like this:

await lh.aspirate96(lh.get_resource('alpha_plate').get_quadrant(3), volume=2.0, 
                    flow_rate=2.0, 
                    transport_air_volume=0, 
                    pre_wetting_volume=0,
                    offset=Coordinate(x=1.5, y=1.5, z=-2.5))

await lh.dispense96(lh.get_resource('alpha_plate').get_quadrant(4), volume=2.0, 
                    flow_rate=2.0,
                    jet=True,
                    offset=Coordinate(x=1.5, y=1.5, z=-0.5))

await lh.aspirate96(lh.get_resource('alpha_plate').get_quadrant(4), volume=0.0, 
                    flow_rate=2.0,
                    transport_air_volume=0, 
                    pre_wetting_volume=0,
                    homogenization_volume = 10, 
                    homogenization_cycles = 2, 
                    speed_of_homogenization = 10,
                    offset=Coordinate(x=1.5, y=1.5, z=-2))

When it gets to the second aspirate96 (which is the mix cycle, note volume=0.0, see Mixing during dispense - #4 by ben) it gives the following error:

---------------------------------------------------------------------------
STARFirmwareError                         Traceback (most recent call last)
Cell In[13], line 1
----> 1 await lh.aspirate96(lh.get_resource('alpha_plate').get_quadrant(4), volume=0.0, 
      2                     flow_rate=1.0,
      3                     transport_air_volume=0.0, 
      4                     pre_wetting_volume=0.0,
      5                     homogenization_volume = 10, 
      6                     homogenization_cycles = 2, 
      7                     speed_of_homogenization = 10,
      8                     offset=Coordinate(x=1.5, y=1.5, z=-2))

File ~/Documents/FutureHouse/Lab/Automation/pylabrobot/pylabrobot/liquid_handling/liquid_handler.py:1456, in LiquidHandler.aspirate96(self, resource, volume, offset, flow_rate, blow_out_air_volume, **backend_kwargs)
   1454       well.tracker.rollback()
   1455     channel.get_tip().tracker.rollback()
-> 1456   self._trigger_callback(
   1457     "aspirate96",
   1458     liquid_handler=self,
   1459     aspiration=aspiration,
   1460     error=error,
   1461     **backend_kwargs,
   1462   )
   1463 else:
   1464   for channel, well in zip(self.head96.values(), wells):
...
   1208     he.errors[module_name].message += \
   1209       " (call lh.backend.request_name_of_last_faulty_parameter)"
-> 1211 raise he

STARFirmwareError: {'CoRe 96 Head': HardwareError('Maximum volume in tip reached')}, C0EAid0013er99/00 H002/53
Output is truncated. View as a scrollable element or open in a text editor. Adjust cell output settings...

It seems like for some reason it thinks there is still volume in the tip after the prior dispense.

Some other details:
If I run the mix block (aspirate with volume=0) by itself, it works fine.
I swear that I ran the whole block at least once and it worked fine and has now decided to not work. I have no idea what that means, and I could be wrong.

In my experience, this error is typically due to the volume correction curve in your liquid class being different than the actual volume you ask pylabrobot to aspirate or dispense. if you have several asp/dsp or mix with the same tips in series, this difference can become great enough to exceed the pipette stroke volume.

I haven’t used the 96ch head in this way yet, so I could be wrong.

One other oddity to note - even if the volume from the prior dispense cycle was not getting updated (zeroed out), I don’t understand the max volume issue. According to Hamilton the tip capacity of the 10 uL filter tips (LTF in PLR) is a full 10 uL, and I’m doing a 1 uL mix (homogenization_volume = 10)

I’ve tried setting the mix volume lower to 2 or 3 and it will go through the motions but 0.2 uL is not really effective mixing :upside_down_face:. If I set it at 5 it will throw the same error.

You might be able to pass in a custom volume correction curve to use for mixing, which mixes exactly the amount you need rather than ± some amount. Let me know if you need help with this

This is good to note. I’m not setting any liquid class explicitly and am just using water right now. Should probably learn more about how PLR does liquid classes…

It looks like the tip & volume tracker issues that I was running into (with an older version of PLR) are correct now. So I don’t think it’s quite the same issue.

What are these parameters? Is that error coming from the tip volume tracker or from the STAR?

homogenization_volume = 10,
homogenization_cycles = 2,

Those parameters are backend kwargs passed to aspirate96 that set the mixing volume (homogenization_volume, in units of 0.1 uL) and number of mix cycles (homogenization_cycles).

You triggered something for me with your other question. I didn’t think or look hard enough at the error message, and now I see it is indeed an error coming from the STAR as a firmware/hardware error, not a tracker error. Unfortunately, H0 error 53 is the only H0 error without a “possible cause” entry. It’s triggered by “Supervision.”

This def makes me more inclined to agree with @ben that it may be a liquid class problem of some sort, although the volume is so small relative to the tip volume I don’t really understand it. @ben if you can provide some further assistance or suggestion as offered, it would be much appreciated. Do you think it has something to do with my other kwargs (transport_air_volume, pre_wetting_volume, etc)?

  • some corrections:

The error you’re seeing is from the robot’s volume/hardware tracking, not PLR’s (your issue @RKeyser was with PLR trackers).

Volume correction curves are not applied to homogenization_volume. Maybe that’s a mistake, but that is the case right now.

the units have actually been made consistent now: make units in STAR method parameters consistent by rickwierenga · Pull Request #191 · PyLabRobot/pylabrobot · GitHub

explicitly:

(homogenization_volume , in units of 0.1 uL)

is no longer true.

This is true if the volumes in the aspiration/dispense commands don’t perfectly cancel out because correct(volume*N) != N*correct(volume) (function is non-linear). In this case Jon is doing vol=2 for both.

I will attempt to replicate issue today

Thanks @rickwierenga! I was actually starting to wonder if if the homogenization volumes got modified recently to match the others, as I was seeing some weird behavior with it but couldn’t find a specific PR just browsing, so thanks for pointing me to that.

Let me know if I can help with reproducing anything today, or if I should break this out in a new thread given the divergence.

@rickwierenga and everyone else here: Given Rick’s update that the backend kwarg volumes were fixed a while back, this is now working as expected as far as going through the motions with the appropriate volumes specified.

A couple weird things still remain:

  • I do not see volume fluctuating to the extent I expect when I watch the wells, even up to specifying a 10 uL homogenization volume. I can see maybe 3-4 uL of volume fluctuation at that setting (I’m mixing a 10 uL total volume.) Given enough cycles this is probably sufficient to work with for now.

  • I still don’t know why yesterday’s volume setting worked when running the mix cycle by itself, but did not work when run after the first two asp/disp cycles. I tried it again today and did not recapitulate the error from yesterday even with the same volumes…

that’s odd, but must be a machine error if you verify that the right firmware command is sent

so this is now working as expected, but you couldn’t reproduce the issue from yesterday?

I truly have no idea what is happening. There is a longer story and some other details I can get into, but it’s hard to make sense of them all.

The short story is this might have something to do with the jet dispense step, but might not.

I came in this morning and made some minor changes to the code after reading the note from @rickwierenga. That code is pasted below for your viewing pleasure. This code worked just fine right away and I had no problems even going up to the same volumes as the code yesterday. The only two changes I made:

  1. I changed the jet=True to blow_out=True (but had actually done this yesterday and was getting the error.) This is also in the dispense step prior to the error-inducing mix step.
  2. I noticed I was mixing floats and ints when calling volumes. I know PLR corrects for int calls, so I changed them all to ints to be consistent.

I then tried to run exactly the same code from yesterday and immediately got the error. I then re-ran the “good” new code without error. I then ran the old “bad” code and did not get the error, even running it many times in succession.

To sum up, what I seem to be able to reproduce is:

pick up tips
run "bad" code => get error
run "good" code => no error
run "bad" code => no error
drop tips

and

pick up tips
run "good" code => no error
run "bad" code => no error
drop tips

Something in the original “bad” code is causing an issue, and running the new “good” code is fixing it, seemingly indefinitely.

Now, I have the functioning code and it works for me so I can continue working on this process, but I leave this all here for people to think on. I’m happy to keep experimenting to figure things out, let me know. For reference here again are the code blocks:

Bad:

await lh.aspirate96(lh.get_resource('alpha_plate').get_quadrant(3), volume=2.0, 
                    flow_rate=2.0, 
                    transport_air_volume=0, 
                    pre_wetting_volume=0,
                    offset=Coordinate(x=1.5, y=1.5, z=-2.5))

await lh.dispense96(lh.get_resource('alpha_plate').get_quadrant(4), volume=2.0, 
                    flow_rate=2.0,
                    jet=True,
                    offset=Coordinate(x=1.5, y=1.5, z=-0.5))

await lh.aspirate96(lh.get_resource('alpha_plate').get_quadrant(4), volume=0.0, 
                    flow_rate=2.0,
                    transport_air_volume=0, 
                    pre_wetting_volume=0,
                    homogenization_volume = 8, 
                    homogenization_cycles = 2, 
                    speed_of_homogenization = 8,
                    offset=Coordinate(x=1.5, y=1.5, z=-2))

Good:

await lh.aspirate96(lh.get_resource('alpha_plate').get_quadrant(3), volume=2, 
                    flow_rate=4, 
                    transport_air_volume=0, 
                    pre_wetting_volume=0,
                    offset=Coordinate(x=1.5, y=1.5, z=-3))

await lh.dispense96(lh.get_resource('alpha_plate').get_quadrant(4), volume=2, 
                    flow_rate=4,
                    blow_out=True,
                    offset=Coordinate(x=1.5, y=1.5, z=-0.5))

await lh.aspirate96(lh.get_resource('alpha_plate').get_quadrant(4), volume=0, 
                    flow_rate=8,
                    transport_air_volume=0,
                    pre_wetting_volume=0,
                    homogenization_volume = 8,
                    homogenization_cycles = 4,
                    speed_of_homogenization = 8,
                    offset=Coordinate(x=1.5, y=1.5, z=-3))