Skip to content
Open
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
70 changes: 41 additions & 29 deletions pylabrobot/hamilton/star/driver/features/head96.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,6 @@ class Head96Configuration(HeadConfiguration):
first_documented_firmware_year: int = 2010

# As on the base: what the head reported, standing in front of the derived values below.
dispensing_drive_speed_firmware_reported: Optional[float] = None
dispensing_drive_acceleration_firmware_reported: Optional[float] = None
squeezer_drive_speed_firmware_reported: Optional[float] = None
squeezer_drive_acceleration_firmware_reported: Optional[float] = None

Expand Down Expand Up @@ -223,13 +221,6 @@ def dispensing_drive_speed_range(self) -> Tuple[float, float]:
self.dispensing_drive_increments_to_uL(max_inc),
)

@property
def dispensing_drive_speed_default(self) -> float:
"""Dispensing-drive default speed (uL/s); constant across firmware."""
if self.dispensing_drive_speed_firmware_reported is not None:
return self.dispensing_drive_speed_firmware_reported
return 261.1

@property
def dispensing_drive_acceleration_range(self) -> Tuple[float, float]:
"""Dispensing-drive acceleration window (uL/s2); its max is the default 2013 firmware raised."""
Expand All @@ -239,14 +230,6 @@ def dispensing_drive_acceleration_range(self) -> Tuple[float, float]:
self.dispensing_drive_increments_to_uL(max_inc),
)

@property
def dispensing_drive_acceleration_default(self) -> float:
"""Dispensing-drive default acceleration (uL/s2); 2013 firmware raised it."""
if self.dispensing_drive_acceleration_firmware_reported is not None:
return self.dispensing_drive_acceleration_firmware_reported
increments = 900000 if self.firmware_year >= 2010 else 150000
return self.dispensing_drive_increments_to_uL(increments)

@property
def squeezer_drive_speed_default(self) -> float:
"""Squeezer-drive default speed (mm/s); 2013 firmware raised it."""
Expand Down Expand Up @@ -295,6 +278,9 @@ class Head96(Head):
default_mix_position_from_liquid_surface: float = 2.0
# Aspirate under cLLD: how far below the surface found the tips draw, in mm.
default_aspirate_immersion_depth: float = 2.0
# Behind the properties of the same names, which check a value against its range when assigned.
_default_dispensing_drive_speed: float = 250.0
_default_dispensing_drive_acceleration: float = 17000.0
# How far above a container's top a liquid search starts, in mm.
search_start_clearance: float = 5.0

Expand All @@ -308,6 +294,34 @@ def __init__(self, driver: "STARDriver", configuration: Optional[Head96Configura
# Where the piston stands, in uL, as last read or moved; 0.0 at rest.
self.piston_position: float = 0.0

@property
def default_dispensing_drive_speed(self) -> float:
"""In uL/s. Not read from the head, which answers with the speed its last command set."""
return self._default_dispensing_drive_speed

@default_dispensing_drive_speed.setter
def default_dispensing_drive_speed(self, speed: float) -> None:
low, high = self.configuration.dispensing_drive_speed_range
if not low <= speed <= high:
raise ValueError(
f"default_dispensing_drive_speed must be between {low} and {high}, is {speed}"
)
self._default_dispensing_drive_speed = speed

@property
def default_dispensing_drive_acceleration(self) -> float:
"""In uL/s2. A head with firmware from before 2010 gets 2900.0 at setup."""
return self._default_dispensing_drive_acceleration

@default_dispensing_drive_acceleration.setter
def default_dispensing_drive_acceleration(self, acceleration: float) -> None:
low, high = self.configuration.dispensing_drive_acceleration_range
if not low <= acceleration <= high:
raise ValueError(
f"default_dispensing_drive_acceleration must be between {low} and {high}, is {acceleration}"
)
self._default_dispensing_drive_acceleration = acceleration

# -- session / discovery -------------------------------------------------------------------------

def _apply_firmware_generation(self) -> None:
Expand All @@ -320,17 +334,16 @@ def _apply_firmware_generation(self) -> None:
c = self.configuration
if c.firmware_year >= 2010:
return
self.default_dispensing_drive_acceleration = 2900.0
c.y_drive_acceleration_mm_per_increment = c.y_drive_mm_per_increment * 1000
c.z_drive_acceleration_mm_per_increment = c.z_drive_mm_per_increment * 1000
c.drive_parameters = {"yv": 5, "yr": 3, "zv": 5, "zr": 3, "dv": 5, "dr": 4, "sv": 5, "sr": 3}
c.z_acceleration_range_increments = (5, 100)

async def discover(self):
"""Read what head this is, then take its dispensing and squeezer defaults from the head."""
"""Read what head this is, then take its squeezer defaults from the head."""
await super().discover()
c = self.configuration
c.dispensing_drive_speed_firmware_reported = await self._reported_drive_parameter("dv")
c.dispensing_drive_acceleration_firmware_reported = await self._reported_drive_parameter("dr")
c.squeezer_drive_speed_firmware_reported = await self._reported_drive_parameter("sv")
c.squeezer_drive_acceleration_firmware_reported = await self._reported_drive_parameter("sr")

Expand Down Expand Up @@ -387,10 +400,9 @@ async def move_dispensing_drive_to_position(

Args:
volume: where to send the piston, as the volume it would hold, in uL.
speed: how fast, in uL/s. Defaults to `configuration.dispensing_drive_speed_default`.
speed: how fast, in uL/s. Defaults to `default_dispensing_drive_speed`.
stop_speed: what to slow to at the end, in uL/s.
acceleration: how hard, in uL/s2. Defaults to
`configuration.dispensing_drive_acceleration_default`.
acceleration: how hard, in uL/s2. Defaults to `default_dispensing_drive_acceleration`.
current_limit: the motor current limit.
read_timeout: how long to wait for the device to answer, in seconds.

Expand All @@ -399,9 +411,9 @@ async def move_dispensing_drive_to_position(
"""
c = self.configuration
if speed is None:
speed = c.dispensing_drive_speed_default
speed = self.default_dispensing_drive_speed
if acceleration is None:
acceleration = c.dispensing_drive_acceleration_default
acceleration = self.default_dispensing_drive_acceleration

for value, (low, high), name in (
(volume, c.dispensing_drive_range, "volume"),
Expand Down Expand Up @@ -1397,7 +1409,7 @@ async def _aspirate_in_place(

Args:
volume: per channel, in uL.
flow_rate: in uL/s. `dispensing_drive_speed_default` when None.
flow_rate: in uL/s. `default_dispensing_drive_speed` when None.
surface_following_distance: how far down it follows the surface, in mm.
minimum_height: lowest tip bottom height, in mm. The lowest reachable when None.

Expand All @@ -1406,7 +1418,7 @@ async def _aspirate_in_place(
"""
c = self.configuration
if flow_rate is None:
flow_rate = c.dispensing_drive_speed_default
flow_rate = self.default_dispensing_drive_speed
following_max = c.z_drive_increments_to_mm(9999)
for checked, (low, high), name in (
(volume, c.dispensing_drive_range, "volume"),
Expand Down Expand Up @@ -1448,7 +1460,7 @@ async def _dispense_in_place(

Args:
volume: per channel, in uL.
flow_rate: in uL/s. `dispensing_drive_speed_default` when None.
flow_rate: in uL/s. `default_dispensing_drive_speed` when None.
stop_flow_rate: in uL/s.
stop_back_volume: drawn back at the end, in uL.
surface_following_distance: how far up it follows the surface, in mm.
Expand All @@ -1459,7 +1471,7 @@ async def _dispense_in_place(
"""
c = self.configuration
if flow_rate is None:
flow_rate = c.dispensing_drive_speed_default
flow_rate = self.default_dispensing_drive_speed
speed_max = c.dispensing_drive_speed_range[1]
following_max = c.z_drive_increments_to_mm(9999)
for checked, (low, high), name in (
Expand Down
38 changes: 29 additions & 9 deletions pylabrobot/hamilton/star/driver/features/head_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ async def test_the_defaults_are_what_the_head_reported(self):
(200.0, 300.0, 50.0, 250.0),
)

async def test_the_96_head_takes_its_dispensing_and_squeezer_defaults_too(self):
"""`Head96.discover` reads four drive parameters on top of the Y and Z ones every head shares,
async def test_the_96_head_takes_its_squeezer_defaults_too(self):
"""`Head96.discover` reads two drive parameters on top of the Y and Z ones every head shares,
and its defaults answer with what it reported for them. Apart from the test above because it
covers the override rather than the base: the 384-head adds no reads of its own.

Expand All @@ -96,8 +96,6 @@ async def test_the_96_head_takes_its_dispensing_and_squeezer_defaults_too(self):
decoded rather than handed over whole."""
declared = dataclasses.replace(
RECORDED_HEAD96,
dispensing_drive_speed_firmware_reported=400.0,
dispensing_drive_acceleration_firmware_reported=9000.0,
squeezer_drive_speed_firmware_reported=12.0,
squeezer_drive_acceleration_firmware_reported=50.0,
)
Expand All @@ -109,15 +107,37 @@ async def test_the_96_head_takes_its_dispensing_and_squeezer_defaults_too(self):
c = cast(Head96, driver.x_arm.head96).configuration
for read, declared_value in zip(
(
c.dispensing_drive_speed_default,
c.dispensing_drive_acceleration_default,
c.squeezer_drive_speed_default,
c.squeezer_drive_acceleration_default,
),
(400.0, 9000.0, 12.0, 50.0),
(12.0, 50.0),
):
self.assertAlmostEqual(read, declared_value, places=1)

async def test_a_head_with_firmware_from_before_2010_gets_its_lower_dispensing_acceleration(self):
declared = dataclasses.replace(RECORDED_HEAD96, firmware_version="1.0S 2008-11-11 (H0)")
driver = STARSimulationDriver(
deck=STARDeck(), declared_configuration_json=declaring(head96=declared)
)
await driver.setup()
head = cast(Head96, driver.x_arm.head96)
self.assertEqual(head.default_dispensing_drive_acceleration, 2900.0)

async def test_a_default_outside_the_range_of_its_drive_is_refused_when_assigned(self):
driver = STARSimulationDriver(deck=STARDeck(), declared_configuration_json=RECORDING_STAR)
await driver.setup()
head = cast(Head96, driver.x_arm.head96)
with self.assertRaisesRegex(ValueError, "default_dispensing_drive_speed must be between"):
head.default_dispensing_drive_speed = 5000.0
with self.assertRaisesRegex(ValueError, "default_dispensing_drive_acceleration must be"):
head.default_dispensing_drive_acceleration = 50.0
self.assertEqual(
(head.default_dispensing_drive_speed, head.default_dispensing_drive_acceleration),
(250.0, 17000.0),
)
head.default_dispensing_drive_speed = 200.0
self.assertEqual(head.default_dispensing_drive_speed, 200.0)

async def test_a_head_that_will_not_say_keeps_what_its_firmware_documents(self):
"""A head that refuses the read leaves discovery with nothing to record, and the defaults fall
back to the increments its firmware documents rather than the read failing setup. Driven
Expand Down Expand Up @@ -207,10 +227,10 @@ async def test_pick_up_and_drop_send_what_legacy_sends(self):
self.sent,
[
"C0TTtt01tf1tl0519tv03600tg2tu0",
"H0DQdq11281dv13500du00000dr900000dw15",
"H0DQdq11281dv12926du00000dr878965dw15",
"C0EPxs01179xd0yh2418tt01wu0za2164zh2450ze2450",
"C0ERxs01179xd0yh2418za2164zh2450ze2450",
"H0DQdq11281dv13500du00000dr900000dw15",
"H0DQdq11281dv12926du00000dr878965dw15",
"C0EPxs01179xd0yh2418tt01wu0za2164zh2450ze2450",
# The trash drop centres the array; legacy put A1 4.5 mm right and 58.5 mm forward of this.
"C0ERxs00465xd1yh1788za2164zh2450ze2450",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,6 @@
"y_drive_acceleration_firmware_reported": null,
"z_drive_speed_firmware_reported": null,
"z_drive_acceleration_firmware_reported": null,
"dispensing_drive_speed_firmware_reported": null,
"dispensing_drive_acceleration_firmware_reported": null,
"squeezer_drive_speed_firmware_reported": null,
"squeezer_drive_acceleration_firmware_reported": null,
"stop_disc_type": "core_ii",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,6 @@
"y_drive_acceleration_firmware_reported": null,
"z_drive_speed_firmware_reported": null,
"z_drive_acceleration_firmware_reported": null,
"dispensing_drive_speed_firmware_reported": null,
"dispensing_drive_acceleration_firmware_reported": null,
"squeezer_drive_speed_firmware_reported": null,
"squeezer_drive_acceleration_firmware_reported": null,
"stop_disc_type": "core_ii",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,6 @@
"y_drive_acceleration_firmware_reported": null,
"z_drive_speed_firmware_reported": null,
"z_drive_acceleration_firmware_reported": null,
"dispensing_drive_speed_firmware_reported": null,
"dispensing_drive_acceleration_firmware_reported": null,
"squeezer_drive_speed_firmware_reported": null,
"squeezer_drive_acceleration_firmware_reported": null,
"stop_disc_type": "core_ii",
Expand Down
5 changes: 3 additions & 2 deletions pylabrobot/hamilton/star/driver/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -1010,9 +1010,10 @@ def _simulated_drive_parameter(self, parameter: str) -> float:
# The dispensing and squeezer drives have no register to read, so they answer with what this
# head's firmware documents.
head = self._declared
newer = head.firmware_year >= 2010
documented = {
"dv": head.dispensing_drive_speed_default,
"dr": head.dispensing_drive_acceleration_default,
"dv": 261.1,
"dr": head.dispensing_drive_increments_to_uL(900000 if newer else 150000),
"sv": head.squeezer_drive_speed_default,
"sr": head.squeezer_drive_acceleration_default,
}
Expand Down
Loading