Open
Description
The implementation of LiquidHandler._run_async_in_thread
is not working if the called method actually performs some async stuff.
MWE:
# %%
from dummy_backend import DummyBackend
from pylabrobot.liquid_handling import LiquidHandler
from pylabrobot.resources import (
TIP_CAR_480_A00,
STARDeck,
standard_volume_tip_no_filter,
)
deck = STARDeck()
deck.assign_child_resource(TIP_CAR_480_A00("tip-carrier"), rails=5)
backend = DummyBackend()
lh = LiquidHandler(backend, deck)
await lh.setup()
With the following dummy_backend.py
import asyncio
from typing import List, Union
from pylabrobot.liquid_handling.backends.backend import (
LiquidHandlerBackend,
)
from pylabrobot.liquid_handling.standard import (
Aspiration,
AspirationContainer,
AspirationPlate,
Dispense,
DispenseContainer,
DispensePlate,
Drop,
DropTipRack,
Move,
Pickup,
PickupTipRack,
)
from pylabrobot.resources import Resource
class DummyBackend(LiquidHandlerBackend):
"""The MLPrep backend."""
def __init__(self):
super().__init__()
self._future = None
# --- Liquid Handling Methods, Required by PLR ---
async def setup(self):
async def some_background_task():
await asyncio.sleep(1)
self._task = asyncio.create_task(some_background_task())
await super().setup()
async def stop(self):
super().stop()
async def assigned_resource_callback(self, resource: Resource):
"""Called when a new resource was assigned to the robot.
This callback will also be called immediately after the setup method has been called for any
resources that were assigned to the robot before it was set up. The first resource will always
be the deck itself.
Args:
resource: The resource that was assigned to the robot.
"""
await super().assigned_resource_callback(resource)
print(f"Assigned resource: {resource.name}")
await self._task
async def unassigned_resource_callback(self, name: str):
"""Called when a resource is unassigned from the robot.
Args:
resource: The name of the resource that was unassigned from the robot.
"""
await super().unassigned_resource_callback(name)
print(f"Unassigned resource: {name}")
@property
def num_channels(self) -> int:
"""The number of channels that the robot has."""
return 2
async def pick_up_tips(self, ops: List[Pickup], use_channels: List[int]):
"""Pick up tips from the specified resource."""
raise NotImplementedError()
async def drop_tips(self, ops: List[Drop], use_channels: List[int]):
"""Drop tips from the specified resource."""
raise NotImplementedError()
async def aspirate(self, ops: List[Aspiration], use_channels: List[int]):
"""Aspirate liquid from the specified resource using pip."""
raise NotImplementedError()
async def dispense(self, ops: List[Dispense], use_channels: List[int]):
"""Dispense liquid from the specified resource using pip."""
raise NotImplementedError()
async def pick_up_tips96(self, pickup: PickupTipRack):
"""Pick up tips from the specified resource using CoRe 96."""
raise NotImplementedError()
async def drop_tips96(self, drop: DropTipRack):
"""Drop tips to the specified resource using CoRe 96."""
raise NotImplementedError()
async def aspirate96(self, aspiration: Union[AspirationPlate, AspirationContainer]):
"""Aspirate from all wells in 96 well plate."""
raise NotImplementedError()
async def dispense96(self, dispense: Union[DispensePlate, DispenseContainer]):
"""Dispense to all wells in 96 well plate."""
raise NotImplementedError()
async def move_resource(self, move: Move):
"""Move a resource to a new location."""
raise NotImplementedError()
async def prepare_for_manual_channel_operation(self, channel: int):
"""Prepare the robot for manual operation."""
raise NotImplementedError()
async def move_channel_x(self, channel: int, x: float):
"""Move the specified channel to the specified x coordinate."""
raise NotImplementedError()
async def move_channel_y(self, channel: int, y: float):
"""Move the specified channel to the specified y coordinate."""
raise NotImplementedError()
async def move_channel_z(self, channel: int, z: float):
"""Move the specified channel to the specified z coordinate."""
raise NotImplementedError()
Running this code will lead to the following exception (only copied the relevant part):
Exception in thread Thread-3 (callback):
Traceback (most recent call last):
File "C:\Python312\Lib\threading.py", line 1052, in _bootstrap_inner
self.run()
File "d:\...\.venv\Lib\site-packages\ipykernel\ipkernel.py", line 766, in run_closure
_threading_Thread_run(self)
File "C:\Python312\Lib\threading.py", line 989, in run
self._target(*self._args, **self._kwargs)
File "D:\...\pylabrobot\liquid_handling\liquid_handler.py", line 218, in callback
loop.run_until_complete(func(*args, **kwargs))
File "C:\Python312\Lib\asyncio\base_events.py", line 664, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^
File "d:\....\dummy_backend.py", line 51, in assigned_resource_callback
await self._future
RuntimeError: Task <Task pending name='Task-6' coro=<DummyBackend.assigned_resource_callback() running at [d:\...\dummy_backend.py:51](file:///D:/PREP_VM/PLR-Demo/dummy_backend.py:51)> cb=[_run_until_complete_cb() at [C:\Python312\Lib\asyncio\base_events.py:180](file:///C:/Python312/Lib/asyncio/base_events.py:180)]> got Future <Future pending> attached to a different loop
Metadata
Metadata
Assignees
Labels
No labels