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:
- 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).
- The 21 mm search window can't close the remaining gap even with
the correct direction.
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.
Summary
On
pylabrobot==0.2.1,pick_up_tipsagainst a stockDiTi_3Poscarrier
holding a stock
DiTi_1000ul_LiHatip rack fails on real Tecan EVO 200hardware 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 themaintainer's own
# TODO: verify z formulaflag, which matches what weobserve.
Environment
pylabrobot==0.2.1(also reproduces againstmainas of 2026-05-21,same formula present)
libusb_package==1.0.26.3Repro (minimal)
Analysis
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:
toward it. With z_off = 0 the formula would give
_z_range − 87.7 mm (closer, though still ~16 mm short).
the correct direction.
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.