Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,25 @@ class Head96Information:
"""Dispensing-drive (piston) volume window (uL); applies to both aspirate and dispense."""
dispensing_drive_speed_range: Tuple[float, float]
"""Dispensing-drive speed window (uL/s)."""
# Per-drive default speed / acceleration that vary by firmware version (resolved at setup).
y_drive_speed_default: float
"""Y-drive default speed (mm/s)."""
y_drive_acceleration_default: float
"""Y-drive default acceleration (mm/s2)."""
dispensing_drive_acceleration_default: float
"""Dispensing-drive default acceleration (uL/s2)."""
squeezer_drive_speed_default: float
"""Squeezer-drive default speed (mm/s)."""
squeezer_drive_acceleration_default: float
"""Squeezer-drive default acceleration (mm/s2)."""

# === Per-drive default speed / acceleration that are constant across firmware (standard units). ===
z_drive_speed_default: float = 85.0
"""Z-drive default speed (mm/s)."""
z_drive_acceleration_default: float = 400.0
"""Z-drive default acceleration (mm/s2)."""
dispensing_drive_speed_default: float = 261.1
"""Dispensing-drive default speed (uL/s)."""

# === Encoder resolutions (defaulted device facts). Y/Z are unchanged across firmware; the
# dispensing/squeezer resolutions are the 2013+ generation values (2008-era heads differ). ===
Expand Down Expand Up @@ -2098,6 +2117,19 @@ async def set_up_core96_head():
dispensing_drive_speed_range=self._head96_resolve_dispensing_drive_speed_range(
fw_version
),
y_drive_speed_default=self._head96_resolve_y_drive_speed_default(fw_version),
y_drive_acceleration_default=self._head96_resolve_y_drive_acceleration_default(
fw_version
),
dispensing_drive_acceleration_default=self._head96_resolve_dispensing_drive_acceleration_default(
fw_version
),
squeezer_drive_speed_default=self._head96_resolve_squeezer_drive_speed_default(
fw_version
),
squeezer_drive_acceleration_default=self._head96_resolve_squeezer_drive_acceleration_default(
fw_version
),
)

async def set_up_arm_modules():
Expand Down Expand Up @@ -7806,6 +7838,35 @@ def _head96_resolve_dispensing_drive_speed_range(
self._head96_dispensing_drive_increment_to_uL(max_inc),
)

# Per-drive default speed / acceleration that vary by firmware version (the constant ones are plain
# Head96Information fields). 2013 firmware raised them. The dispensing/squeezer values use the 2013+
# encoder resolutions, so for pre-2010 heads they are approximate (as the ranges above are).
def _head96_resolve_y_drive_speed_default(self, fw_version: datetime.date) -> float:
"""Y-drive default speed (mm/s); 2013 firmware raised it."""
return self._head96_y_drive_increment_to_mm(25000 if fw_version.year >= 2010 else 20000)

def _head96_resolve_y_drive_acceleration_default(self, fw_version: datetime.date) -> float:
"""Y-drive default acceleration (mm/s2); 2013 firmware raised it."""
return self._head96_y_drive_increment_to_mm(35000 if fw_version.year >= 2010 else 32000)

def _head96_resolve_dispensing_drive_acceleration_default(
self, fw_version: datetime.date
) -> float:
"""Dispensing-drive default acceleration (uL/s2); 2013 firmware raised it."""
return self._head96_dispensing_drive_increment_to_uL(
900000 if fw_version.year >= 2010 else 150000
)

def _head96_resolve_squeezer_drive_speed_default(self, fw_version: datetime.date) -> float:
"""Squeezer-drive default speed (mm/s); 2013 firmware raised it."""
return self._head96_squeezer_drive_increment_to_mm(76000 if fw_version.year >= 2010 else 16000)

def _head96_resolve_squeezer_drive_acceleration_default(self, fw_version: datetime.date) -> float:
"""Squeezer-drive default acceleration (mm/s2); 2013 firmware raised it."""
return self._head96_squeezer_drive_increment_to_mm(
300000 if fw_version.year >= 2010 else 100000
)

# -------------- 3.10.1 Initialization --------------

async def initialize_core_96_head(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ async def setup(
z_range=self._head96_resolve_z_range(instrument_type),
dispensing_drive_range=self._head96_resolve_dispensing_drive_range(fw_version),
dispensing_drive_speed_range=self._head96_resolve_dispensing_drive_speed_range(fw_version),
y_drive_speed_default=self._head96_resolve_y_drive_speed_default(fw_version),
y_drive_acceleration_default=self._head96_resolve_y_drive_acceleration_default(fw_version),
dispensing_drive_acceleration_default=self._head96_resolve_dispensing_drive_acceleration_default(
fw_version
),
squeezer_drive_speed_default=self._head96_resolve_squeezer_drive_speed_default(fw_version),
squeezer_drive_acceleration_default=self._head96_resolve_squeezer_drive_acceleration_default(
fw_version
),
)
else:
self._head96_information = None
Expand Down
33 changes: 33 additions & 0 deletions pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import contextlib
import copy
import datetime
import unittest
import unittest.mock
from typing import Literal, cast
Expand Down Expand Up @@ -447,6 +448,38 @@ async def test_skipped_when_iswap_not_installed(self):
_ = cb.iswap_information


class TestHead96DriveDefaults(unittest.IsolatedAsyncioTestCase):
"""Head96Information carries the firmware default speed/acceleration for every drive, in standard
units: version-resolved where it changed across firmware, constant fields where it did not."""

async def test_setup_resolves_all_drive_defaults(self):
"""setup() populates all eight per-drive defaults with the 2013+ firmware values."""
cb = STARChatterboxBackend() # mocks a 2023 (2013+) head
cb.set_deck(STARLetDeck())
await cb.setup()
info = cb._head96_information
assert info is not None
self.assertAlmostEqual(info.y_drive_speed_default, 390.62, places=2)
self.assertAlmostEqual(info.y_drive_acceleration_default, 546.88, places=2)
self.assertEqual(info.z_drive_speed_default, 85.0)
self.assertEqual(info.z_drive_acceleration_default, 400.0)
self.assertEqual(info.dispensing_drive_speed_default, 261.1)
self.assertAlmostEqual(info.dispensing_drive_acceleration_default, 17406.84, places=2)
self.assertAlmostEqual(info.squeezer_drive_speed_default, 15.86, places=2)
self.assertAlmostEqual(info.squeezer_drive_acceleration_default, 62.6, places=2)

def test_version_resolved_default_falls_back_for_pre_2010_firmware(self):
"""A version-resolved default switches to the 2008 firmware value for pre-2010 heads (Y, whose
encoder resolution is constant, so both branches are exact)."""
star = STARBackend(read_timeout=1)
self.assertAlmostEqual(
star._head96_resolve_y_drive_speed_default(datetime.date(2008, 11, 11)), 312.5, places=2
)
self.assertAlmostEqual(
star._head96_resolve_y_drive_speed_default(datetime.date(2013, 9, 2)), 390.62, places=2
)


class TestiSWAPYMaxBootstrap(unittest.IsolatedAsyncioTestCase):
"""`_iswap_rotation_drive_request_y_max` runs during setup, before
`iswap_information` exists, so it must not read it (regression: it used to,
Expand Down
Loading