Skip to content

Commit d643aa0

Browse files
authored
vspin: fix bucket1 pos after spin; fix short spins (#760)
1 parent 9fa6ac0 commit d643aa0

2 files changed

Lines changed: 48 additions & 18 deletions

File tree

pylabrobot/centrifuge/centrifuge.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ async def spin(self, g: float, duration: float, **backend_kwargs) -> None:
110110
111111
Args:
112112
g: The g-force to spin at.
113-
duration: The duration of the spin in seconds.
113+
duration: The duration of the spin in seconds. Time at speed.
114114
acceleration: The acceleration as a fraction of maximum acceleration (0-1).
115115
"""
116116
await self.backend.spin(

pylabrobot/centrifuge/vspin_backend.py

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import ctypes
33
import json
44
import logging
5+
import math
56
import os
67
import time
78
import warnings
@@ -279,11 +280,22 @@ async def set_bucket_1_position_to_current(self) -> None:
279280
_save_vspin_calibrations(device_id, remainder)
280281

281282
async def get_bucket_1_position(self) -> int:
282-
"""Get the bucket 1 position based on calibration."""
283+
"""Get the bucket 1 position based on calibration.
284+
Normally it is the home position minus the remainder (calibration).
285+
The bucket 1 position must be greater than the current position, so we find
286+
the first position greater than the current position by adding full rotations if needed.
287+
"""
283288
if self._bucket_1_remainder is None:
284289
raise bucket_1_not_set_error
285290
home_position = await self.get_home_position()
286-
bucket_1_position = (home_position - self.bucket_1_remainder) % FULL_ROTATION
291+
bucket_1_position_mod_full_rotation = home_position - self.bucket_1_remainder
292+
# first number after current position that matches bucket 1 position mod FULL_ROTATION
293+
current_position = await self.get_position()
294+
bucket_1_position = (
295+
FULL_ROTATION
296+
* math.floor((current_position - bucket_1_position_mod_full_rotation) / FULL_ROTATION + 1)
297+
+ bucket_1_position_mod_full_rotation
298+
)
287299
return bucket_1_position
288300

289301
async def stop(self):
@@ -572,18 +584,21 @@ async def spin(
572584
await self._send_command(byte_string)
573585

574586
# 3 - wait for acceleration to the set rpm
575-
while await self.get_tachometer() < rpm * 0.95:
587+
# we also check the position to avoid waiting forever if the speed is not reached (e.g. short spin...)
588+
while await self.get_tachometer() < rpm * 0.95 and await self.get_position() < final_position:
576589
await asyncio.sleep(0.1)
577590

578591
# 4 - once the speed is reached, compute the position at which to start deceleration
579592
# this is different than computed above, because above we assumed constant acceleration from 0 to rpm.
580593
# however, in reality there is jerk and the acceleration is not constant, so we have to adjust as we go.
581594
# this is what the vendor software does too.
582-
decel_start_position = await self.get_position() + distance_at_speed
595+
# if we are already past that position, we skip this part.
596+
if await self.get_position() < final_position:
597+
decel_start_position = await self.get_position() + distance_at_speed
583598

584-
# then wait until we reach that position
585-
while await self.get_position() < decel_start_position:
586-
await asyncio.sleep(0.1)
599+
# then wait until we reach that position
600+
while await self.get_position() < decel_start_position:
601+
await asyncio.sleep(0.1)
587602

588603
# 5 - send deceleration command
589604
await self._send_command(bytes.fromhex("aa01e60500640000000000fd00803e01000c"))
@@ -598,16 +613,31 @@ async def spin(
598613
await asyncio.sleep(2)
599614

600615
# 6 - reset position back to 0ish
601-
# this part is needed because otherwise calling go_to_position will not work after
602-
await self._send_command(bytes.fromhex("aa0117021a"))
603-
await self._send_command(bytes.fromhex("aa01e6c800b00496000f004b00a00f050007"))
604-
await self._send_command(bytes.fromhex("aa0117041c"))
605-
await self._send_command(bytes.fromhex("aa01170119"))
606-
await self._send_command(bytes.fromhex("aa010b0c"))
607-
await self._send_command(bytes.fromhex("aa010001")) # set position back to 0 (exactly)
608-
await self._send_command(bytes.fromhex("aa01e605006400000000003200e80301006e"))
609-
await self._send_command(bytes.fromhex("aa0194b61283000012010000f3"))
610-
await self._send_command(bytes.fromhex("aa01192842")) # it starts moving again
616+
# this part is aneeded because otherwise calling go_to_position will not work after
617+
async def _reset_to_zero():
618+
await self._send_command(bytes.fromhex("aa0117021a"))
619+
await self._send_command(bytes.fromhex("aa01e6c800b00496000f004b00a00f050007"))
620+
await self._send_command(bytes.fromhex("aa0117041c"))
621+
await self._send_command(bytes.fromhex("aa01170119"))
622+
await self._send_command(bytes.fromhex("aa010b0c"))
623+
await self._send_command(bytes.fromhex("aa010001")) # set position back to 0 (exactly)
624+
await self._send_command(bytes.fromhex("aa01e605006400000000003200e80301006e"))
625+
await self._send_command(bytes.fromhex("aa0194b61283000012010000f3"))
626+
await self._send_command(bytes.fromhex("aa01192842")) # it starts moving again
627+
628+
await _reset_to_zero()
629+
630+
# 7 - wait for home position to change
631+
# go_to_bucket{1,2} does not work until the home position changes
632+
start = await self.get_home_position()
633+
num_tries = 0
634+
while await self.get_home_position() == start:
635+
await asyncio.sleep(0.1)
636+
num_tries += 1
637+
if num_tries % 25 == 0:
638+
await _reset_to_zero()
639+
if num_tries > 100:
640+
raise RuntimeError("Home position did not change after spin.")
611641

612642

613643
# Deprecated alias with warning # TODO: remove mid May 2025 (giving people 1 month to update)

0 commit comments

Comments
 (0)