mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-24 09:49:16 +00:00
no opcua installation on macos
fix possible crash fix deck & host_node set liquid with tube add test_resource_schema fix test resource schema registry update & workflow update add test mode support description & tags upload fix config load fix log add registry name & add always free correct config organic synthesis Adapt to new scheduler, sampels, and edge upload format (#230) * add sample_material * adapt to new samples sys * fix pump transfer. fix resource update when protocol & ros callback * Adapt to new scheduler. Feat/samples (#229) * add sample_material * adapt to new samples sys adapt to new samples sys adapt to new edge format workflow upload & prcxi transfer liquid lh liquid speed up registry load workflow upload & set liquid fix & add set liquid with plate fix upload workflow json
This commit is contained in:
@@ -1,15 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import traceback
|
||||
from collections import Counter
|
||||
from typing import List, Sequence, Optional, Literal, Union, Iterator, Dict, Any, Callable, Set, cast
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
from pylabrobot.liquid_handling import LiquidHandler, LiquidHandlerBackend, LiquidHandlerChatterboxBackend, Strictness
|
||||
from unilabos.devices.liquid_handling.rviz_backend import UniLiquidHandlerRvizBackend
|
||||
from unilabos.devices.liquid_handling.laiyu.backend.laiyu_v_backend import UniLiquidHandlerLaiyuBackend
|
||||
from pylabrobot.liquid_handling.liquid_handler import TipPresenceProbingMethod
|
||||
from pylabrobot.liquid_handling.standard import GripDirection
|
||||
from pylabrobot.resources import (
|
||||
@@ -25,28 +21,40 @@ from pylabrobot.resources import (
|
||||
ResourceHolder,
|
||||
Lid,
|
||||
Trash,
|
||||
Tip,
|
||||
Tip, TubeRack,
|
||||
)
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from unilabos.devices.liquid_handling.rviz_backend import UniLiquidHandlerRvizBackend
|
||||
from unilabos.registry.placeholder_type import ResourceSlot
|
||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||
from unilabos.resources.resource_tracker import ResourceTreeSet
|
||||
from unilabos.resources.resource_tracker import (
|
||||
ResourceTreeSet,
|
||||
ResourceDict,
|
||||
EXTRA_SAMPLE_UUID,
|
||||
EXTRA_UNILABOS_SAMPLE_UUID,
|
||||
)
|
||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode
|
||||
|
||||
|
||||
class SimpleReturn(TypedDict):
|
||||
samples: list
|
||||
volumes: list
|
||||
samples: List[List[ResourceDict]]
|
||||
volumes: List[float]
|
||||
|
||||
|
||||
class SetLiquidReturn(TypedDict):
|
||||
wells: list
|
||||
volumes: list
|
||||
wells: List[List[ResourceDict]]
|
||||
volumes: List[float]
|
||||
|
||||
|
||||
class SetLiquidFromPlateReturn(TypedDict):
|
||||
plate: list
|
||||
wells: list
|
||||
volumes: list
|
||||
plate: List[List[ResourceDict]]
|
||||
wells: List[List[ResourceDict]]
|
||||
volumes: List[float]
|
||||
|
||||
|
||||
class TransferLiquidReturn(TypedDict):
|
||||
sources: List[List[ResourceDict]]
|
||||
targets: List[List[ResourceDict]]
|
||||
|
||||
|
||||
class LiquidHandlerMiddleware(LiquidHandler):
|
||||
@@ -228,12 +236,11 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
||||
res_samples = []
|
||||
res_volumes = []
|
||||
for resource, volume, channel in zip(resources, vols, use_channels):
|
||||
res_samples.append(
|
||||
{"name": resource.name, "sample_uuid": resource.unilabos_extra.get("sample_uuid", None)}
|
||||
)
|
||||
sample_uuid_value = resource.unilabos_extra.get(EXTRA_SAMPLE_UUID, None)
|
||||
res_samples.append({"name": resource.name, EXTRA_SAMPLE_UUID: sample_uuid_value})
|
||||
res_volumes.append(volume)
|
||||
self.pending_liquids_dict[channel] = {
|
||||
"sample_uuid": resource.unilabos_extra.get("sample_uuid", None),
|
||||
EXTRA_SAMPLE_UUID: sample_uuid_value,
|
||||
"volume": volume,
|
||||
}
|
||||
return SimpleReturn(samples=res_samples, volumes=res_volumes)
|
||||
@@ -275,10 +282,10 @@ class LiquidHandlerMiddleware(LiquidHandler):
|
||||
res_samples = []
|
||||
res_volumes = []
|
||||
for resource, volume, channel in zip(resources, vols, use_channels):
|
||||
res_uuid = self.pending_liquids_dict[channel]["sample_uuid"]
|
||||
res_uuid = self.pending_liquids_dict[channel][EXTRA_SAMPLE_UUID]
|
||||
self.pending_liquids_dict[channel]["volume"] -= volume
|
||||
resource.unilabos_extra["sample_uuid"] = res_uuid
|
||||
res_samples.append({"name": resource.name, "sample_uuid": res_uuid})
|
||||
resource.unilabos_extra[EXTRA_SAMPLE_UUID] = res_uuid
|
||||
res_samples.append({"name": resource.name, EXTRA_SAMPLE_UUID: res_uuid})
|
||||
res_volumes.append(volume)
|
||||
|
||||
return SimpleReturn(samples=res_samples, volumes=res_volumes)
|
||||
@@ -682,16 +689,20 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
||||
wells=ResourceTreeSet.from_plr_resources(wells, known_newly_created=False).dump(), volumes=res_volumes # type: ignore
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def set_liquid_from_plate(
|
||||
cls, plate: ResourceSlot, well_names: list[str], liquid_names: list[str], volumes: list[float]
|
||||
self, plate: ResourceSlot, well_names: list[str], liquid_names: list[str], volumes: list[float]
|
||||
) -> SetLiquidFromPlateReturn:
|
||||
"""Set the liquid in wells of a plate by well names (e.g., A1, A2, B3).
|
||||
|
||||
如果 liquid_names 和 volumes 为空,但 plate 和 well_names 不为空,直接返回 plate 和 wells。
|
||||
"""
|
||||
assert issubclass(plate.__class__, Plate) or issubclass(plate.__class__, TubeRack) , f"plate must be a Plate, now: {type(plate)}"
|
||||
plate: Union[Plate, TubeRack]
|
||||
# 根据 well_names 获取对应的 Well 对象
|
||||
wells = [plate.get_well(name) for name in well_names]
|
||||
if issubclass(plate.__class__, Plate):
|
||||
wells = [plate.get_well(name) for name in well_names]
|
||||
elif issubclass(plate.__class__, TubeRack):
|
||||
wells = [plate.get_tube(name) for name in well_names]
|
||||
res_volumes = []
|
||||
|
||||
# 如果 liquid_names 和 volumes 都为空,直接返回
|
||||
@@ -706,6 +717,14 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
||||
well.set_liquids([(liquid_name, volume)]) # type: ignore
|
||||
res_volumes.append(volume)
|
||||
|
||||
task = ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{"resources": wells})
|
||||
submit_time = time.time()
|
||||
while not task.done():
|
||||
if time.time() - submit_time > 10:
|
||||
self._ros_node.lab_logger().info(f"set_liquid_from_plate {plate} 超时")
|
||||
break
|
||||
time.sleep(0.01)
|
||||
|
||||
return SetLiquidFromPlateReturn(
|
||||
plate=ResourceTreeSet.from_plr_resources([plate], known_newly_created=False).dump(), # type: ignore
|
||||
wells=ResourceTreeSet.from_plr_resources(wells, known_newly_created=False).dump(), # type: ignore
|
||||
@@ -1111,7 +1130,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
||||
mix_liquid_height: Optional[float] = None,
|
||||
delays: Optional[List[int]] = None,
|
||||
none_keys: List[str] = [],
|
||||
):
|
||||
) -> TransferLiquidReturn:
|
||||
"""Transfer liquid with automatic mode detection.
|
||||
|
||||
Supports three transfer modes:
|
||||
@@ -1251,6 +1270,11 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
|
||||
"Supported modes: 1->N, N->1, or N->N."
|
||||
)
|
||||
|
||||
return TransferLiquidReturn(
|
||||
sources=ResourceTreeSet.from_plr_resources(list(sources), known_newly_created=False).dump(), # type: ignore
|
||||
targets=ResourceTreeSet.from_plr_resources(list(targets), known_newly_created=False).dump(), # type: ignore
|
||||
)
|
||||
|
||||
async def _transfer_one_to_one(
|
||||
self,
|
||||
sources: Sequence[Container],
|
||||
|
||||
@@ -52,6 +52,7 @@ from unilabos.devices.liquid_handling.liquid_handler_abstract import (
|
||||
SimpleReturn,
|
||||
SetLiquidReturn,
|
||||
SetLiquidFromPlateReturn,
|
||||
TransferLiquidReturn,
|
||||
)
|
||||
from unilabos.registry.placeholder_type import ResourceSlot
|
||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||
@@ -90,7 +91,7 @@ class PRCXI9300Deck(Deck):
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, size_x: float, size_y: float, size_z: float, **kwargs):
|
||||
super().__init__(name, size_x, size_y, size_z)
|
||||
super().__init__(size_x, size_y, size_z, name)
|
||||
self.slots = [None] * 16 # PRCXI 9300/9320 最大有 16 个槽位
|
||||
self.slot_locations = [Coordinate(0, 0, 0)] * 16
|
||||
|
||||
@@ -154,25 +155,29 @@ class PRCXI9300Plate(Plate):
|
||||
**kwargs,
|
||||
):
|
||||
# 如果 ordered_items 不为 None,直接使用
|
||||
items = None
|
||||
ordering_param = None
|
||||
if ordered_items is not None:
|
||||
items = ordered_items
|
||||
elif ordering is not None:
|
||||
# 检查 ordering 中的值是否是字符串(从 JSON 反序列化时的情况)
|
||||
# 如果是字符串,说明这是位置名称,需要让 Plate 自己创建 Well 对象
|
||||
# 我们只传递位置信息(键),不传递值,使用 ordering 参数
|
||||
if ordering and isinstance(next(iter(ordering.values()), None), str):
|
||||
# ordering 的值是字符串,只使用键(位置信息)创建新的 OrderedDict
|
||||
# 传递 ordering 参数而不是 ordered_items,让 Plate 自己创建 Well 对象
|
||||
items = None
|
||||
# 使用 ordering 参数,只包含位置信息(键)
|
||||
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
|
||||
if ordering:
|
||||
values = list(ordering.values())
|
||||
value = values[0]
|
||||
if isinstance(value, str):
|
||||
# ordering 的值是字符串,只使用键(位置信息)创建新的 OrderedDict
|
||||
# 传递 ordering 参数而不是 ordered_items,让 Plate 自己创建 Well 对象
|
||||
items = None
|
||||
# 使用 ordering 参数,只包含位置信息(键)
|
||||
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
|
||||
elif value is None:
|
||||
ordering_param = ordering
|
||||
else:
|
||||
# ordering 的值已经是对象,可以直接使用
|
||||
items = ordering
|
||||
ordering_param = None
|
||||
else:
|
||||
items = None
|
||||
ordering_param = None
|
||||
|
||||
# 根据情况传递不同的参数
|
||||
if items is not None:
|
||||
@@ -243,14 +248,15 @@ class PRCXI9300TipRack(TipRack):
|
||||
if ordered_items is not None:
|
||||
items = ordered_items
|
||||
elif ordering is not None:
|
||||
# 检查 ordering 中的值是否是字符串(从 JSON 反序列化时的情况)
|
||||
# 如果是字符串,说明这是位置名称,需要让 TipRack 自己创建 Tip 对象
|
||||
# 我们只传递位置信息(键),不传递值,使用 ordering 参数
|
||||
if ordering and isinstance(next(iter(ordering.values()), None), str):
|
||||
# ordering 的值是字符串,只使用键(位置信息)创建新的 OrderedDict
|
||||
# 检查 ordering 中的值类型来决定如何处理:
|
||||
# - 字符串值(从 JSON 反序列化): 只用键创建 ordering_param
|
||||
# - None 值(从第二次往返序列化): 同样只用键创建 ordering_param
|
||||
# - 对象值(已经是实际的 Resource 对象): 直接作为 ordered_items 使用
|
||||
first_val = next(iter(ordering.values()), None) if ordering else None
|
||||
if not ordering or first_val is None or isinstance(first_val, str):
|
||||
# ordering 的值是字符串或 None,只使用键(位置信息)创建新的 OrderedDict
|
||||
# 传递 ordering 参数而不是 ordered_items,让 TipRack 自己创建 Tip 对象
|
||||
items = None
|
||||
# 使用 ordering 参数,只包含位置信息(键)
|
||||
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
|
||||
else:
|
||||
# ordering 的值已经是对象,可以直接使用
|
||||
@@ -392,14 +398,15 @@ class PRCXI9300TubeRack(TubeRack):
|
||||
items_to_pass = ordered_items
|
||||
ordering_param = None
|
||||
elif ordering is not None:
|
||||
# 检查 ordering 中的值是否是字符串(从 JSON 反序列化时的情况)
|
||||
# 如果是字符串,说明这是位置名称,需要让 TubeRack 自己创建 Tube 对象
|
||||
# 我们只传递位置信息(键),不传递值,使用 ordering 参数
|
||||
if ordering and isinstance(next(iter(ordering.values()), None), str):
|
||||
# ordering 的值是字符串,只使用键(位置信息)创建新的 OrderedDict
|
||||
# 检查 ordering 中的值类型来决定如何处理:
|
||||
# - 字符串值(从 JSON 反序列化): 只用键创建 ordering_param
|
||||
# - None 值(从第二次往返序列化): 同样只用键创建 ordering_param
|
||||
# - 对象值(已经是实际的 Resource 对象): 直接作为 ordered_items 使用
|
||||
first_val = next(iter(ordering.values()), None) if ordering else None
|
||||
if not ordering or first_val is None or isinstance(first_val, str):
|
||||
# ordering 的值是字符串或 None,只使用键(位置信息)创建新的 OrderedDict
|
||||
# 传递 ordering 参数而不是 ordered_items,让 TubeRack 自己创建 Tube 对象
|
||||
items_to_pass = None
|
||||
# 使用 ordering 参数,只包含位置信息(键)
|
||||
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
|
||||
else:
|
||||
# ordering 的值已经是对象,可以直接使用
|
||||
@@ -713,7 +720,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
|
||||
mix_liquid_height: Optional[float] = None,
|
||||
delays: Optional[List[int]] = None,
|
||||
none_keys: List[str] = [],
|
||||
):
|
||||
) -> TransferLiquidReturn:
|
||||
return await super().transfer_liquid(
|
||||
sources,
|
||||
targets,
|
||||
|
||||
Reference in New Issue
Block a user