Skip to content

Tecan EVO pick_up_tips fails on DiTi_3Pos + DiTi_1000ul_LiHa: _liha_positions Z target lands ~35 mm above tip top #1054

@dinukajayalath

Description

@dinukajayalath

Summary

On pylabrobot==0.2.1, pick_up_tips against a stock DiTi_3Pos
carrier
holding a stock DiTi_1000ul_LiHa tip rack fails on real Tecan EVO 200
hardware with:

TecanError: ('Tip not fetched', 'C5', 25)

The LiHa descends roughly 65 mm before the 21 mm tip-presence search
window expires, but the physical tip top sits ~125 mm below LiHa rest.
The descent ends ~35 mm above the actual tip.

The Z formula in _liha_positions (EVO_backend.py:685) carries the
maintainer's own # TODO: verify z formula flag, which matches what we
observe.

Environment

  • pylabrobot==0.2.1 (also reproduces against main as of 2026-05-21,
    same formula present)
  • Hardware: Tecan Freedom EVO 200, 8-channel disposable-tip LiHa
  • Host: macOS, Python 3.13, libusb_package==1.0.26.3

Repro (minimal)

import asyncio
from pylabrobot.liquid_handling import LiquidHandler
from pylabrobot.liquid_handling.backends.tecan import EVOBackend
from pylabrobot.resources.tecan import EVO200Deck, DiTi_3Pos,
DiTi_1000ul_LiHa

async def main():
    deck = EVO200Deck()
    carrier = DiTi_3Pos(name="tips_carrier")
    carrier[2] = DiTi_1000ul_LiHa(name="tips_1000")
    deck.assign_child_at_rail(carrier, rail=5)

    lh = LiquidHandler(backend=EVOBackend(diti_count=8), deck=deck)
    await lh.setup()
    await lh.pick_up_tips(carrier[2].get_resource().get_items("A1:H1"))

asyncio.run(main())

→ TecanError: ('Tip not fetched', 'C5', 25)

Analysis

_liha_positions computes:

# EVO_backend.py:684-685
# TODO: simplify z units
return int(self._z_range - z + z_off * 10 + tip_length)  # TODO: verify z
 formula

pick_up_tips then calls get_disposable_tip(channels, first_z_start - 227,
 210)
— a 21 mm search window starting at first_z_start - 22.7 mm.

For our setup:

Quantity: z_start
Value: 877 (tenths)
Source: DiTi_1000ul_LiHa
────────────────────────────────────────
Quantity: tip_length
Value: 326 (tenths, = 32.6 mm)
Source: DiTi_1000ul_LiHa_tip
────────────────────────────────────────
Quantity: z_off
Value: 9.0 mm
Source: DiTi_3Pos.size_z + site z
────────────────────────────────────────
Quantity: Computed first_z_start
Value: _z_range − 461 (tenths) = _z_range − 46.1 mm
Source:
────────────────────────────────────────
Quantity: Descent start
Value: _z_range − 68.8 mm
Source: first_z_start − 22.7 mm
────────────────────────────────────────
Quantity: Descent end (after 21 mm window)
Value: _z_range − 89.8 mm
Source:
────────────────────────────────────────
Quantity: Observed physical tip top
Value: ~_z_range − 125 mm
Source: hardware

Two observations:

  1. The z_off * 10 term moves the target away from the tip, not
    toward it. With z_off = 0 the formula would give
    _z_range − 87.7 mm (closer, though still ~16 mm short).
  2. The 21 mm search window can't close the remaining gap even with
    the correct direction.
  • Resource dimensions

The resource-side numbers may also be off:

Class: DiTi_3Pos
size_z: 4.5 mm
site z: 4.5 mm
Note: Tecan part 10613022
────────────────────────────────────────
Class: DiTi_1000ul_LiHa
size_z: 22.2 mm
site z: —
Note: z_start = 877, dz = 32.6, tip_length = 32.6

We don't have an isolated measurement to confirm whether z_start = 877
matches the physical box on this carrier, because the broken formula
contaminates any descent observation. A jog/teach pass would
disambiguate.

Local workaround (not a recommended upstream fix)

For our own protocols, we override pick_up_tips to bypass
_liha_positions entirely and rely on the tip-presence sensor:

async def pick_up_tips(self, ops, use_channels):
await self.liha.get_disposable_tip(
self._bin_use_channels(use_channels),
self._z_range, # descent starts at the safe top of LiHa travel
1500, # 150 mm search window
)

This is intentionally a workaround, not an upstream proposal — sensor
descent at safe speed over 150 mm is noticeably slower than descending
fast to a known Z. The right upstream fix probably involves
re-deriving the Z formula and/or re-measuring resource constants on
hardware, which we're not in a position to do generically.

Happy to test fixes on our EVO 200 if that helps.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions