Compare commits

...

2 Commits

Author SHA1 Message Date
Xuwznln
e8d1263488 workflow upload & prcxi transfer liquid 2026-02-03 18:10:32 +08:00
Xuwznln
380b39100d lh liquid 2026-02-03 15:15:57 +08:00
8 changed files with 1069 additions and 138 deletions

View File

@@ -46,7 +46,7 @@ requirements:
- jinja2 - jinja2
- requests - requests
- uvicorn - uvicorn
- opcua - opcua # [not osx]
- pyserial - pyserial
- pandas - pandas
- pymodbus - pymodbus

View File

@@ -1,15 +1,11 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import time import time
import traceback import traceback
from collections import Counter from collections import Counter
from typing import List, Sequence, Optional, Literal, Union, Iterator, Dict, Any, Callable, Set, cast 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 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.liquid_handler import TipPresenceProbingMethod
from pylabrobot.liquid_handling.standard import GripDirection from pylabrobot.liquid_handling.standard import GripDirection
from pylabrobot.resources import ( from pylabrobot.resources import (
@@ -27,26 +23,33 @@ from pylabrobot.resources import (
Trash, Trash,
Tip, Tip,
) )
from typing_extensions import TypedDict
from unilabos.devices.liquid_handling.rviz_backend import UniLiquidHandlerRvizBackend
from unilabos.registry.placeholder_type import ResourceSlot from unilabos.registry.placeholder_type import ResourceSlot
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDict
from unilabos.resources.resource_tracker import ResourceTreeSet from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode
class SimpleReturn(TypedDict): class SimpleReturn(TypedDict):
samples: list samples: List[List[ResourceDict]]
volumes: list volumes: List[float]
class SetLiquidReturn(TypedDict): class SetLiquidReturn(TypedDict):
wells: list wells: List[List[ResourceDict]]
volumes: list volumes: List[float]
class SetLiquidFromPlateReturn(TypedDict): class SetLiquidFromPlateReturn(TypedDict):
plate: list plate: List[List[ResourceDict]]
wells: list wells: List[List[ResourceDict]]
volumes: list volumes: List[float]
class TransferLiquidReturn(TypedDict):
sources: List[List[ResourceDict]]
targets: List[List[ResourceDict]]
class LiquidHandlerMiddleware(LiquidHandler): class LiquidHandlerMiddleware(LiquidHandler):
@@ -682,14 +685,17 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
wells=ResourceTreeSet.from_plr_resources(wells, known_newly_created=False).dump(), volumes=res_volumes # type: ignore wells=ResourceTreeSet.from_plr_resources(wells, known_newly_created=False).dump(), volumes=res_volumes # type: ignore
) )
@classmethod
def set_liquid_from_plate( def set_liquid_from_plate(
cls, plate: ResourceSlot, well_names: list[str], liquid_names: list[str], volumes: list[float] self, plate: List[ResourceSlot], well_names: list[str], liquid_names: list[str], volumes: list[float]
) -> SetLiquidFromPlateReturn: ) -> SetLiquidFromPlateReturn:
"""Set the liquid in wells of a plate by well names (e.g., A1, A2, B3). """Set the liquid in wells of a plate by well names (e.g., A1, A2, B3).
如果 liquid_names 和 volumes 为空,但 plate 和 well_names 不为空,直接返回 plate 和 wells。 如果 liquid_names 和 volumes 为空,但 plate 和 well_names 不为空,直接返回 plate 和 wells。
""" """
if isinstance(plate, list): # 未来移除
plate = plate[0]
assert issubclass(plate.__class__, Plate), "plate must be a Plate"
plate: Plate = cast(Plate, plate)
# 根据 well_names 获取对应的 Well 对象 # 根据 well_names 获取对应的 Well 对象
wells = [plate.get_well(name) for name in well_names] wells = [plate.get_well(name) for name in well_names]
res_volumes = [] res_volumes = []
@@ -706,6 +712,14 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
well.set_liquids([(liquid_name, volume)]) # type: ignore well.set_liquids([(liquid_name, volume)]) # type: ignore
res_volumes.append(volume) 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( return SetLiquidFromPlateReturn(
plate=ResourceTreeSet.from_plr_resources([plate], known_newly_created=False).dump(), # type: ignore 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 wells=ResourceTreeSet.from_plr_resources(wells, known_newly_created=False).dump(), # type: ignore
@@ -1111,7 +1125,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
mix_liquid_height: Optional[float] = None, mix_liquid_height: Optional[float] = None,
delays: Optional[List[int]] = None, delays: Optional[List[int]] = None,
none_keys: List[str] = [], none_keys: List[str] = [],
): ) -> TransferLiquidReturn:
"""Transfer liquid with automatic mode detection. """Transfer liquid with automatic mode detection.
Supports three transfer modes: Supports three transfer modes:
@@ -1251,6 +1265,11 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
"Supported modes: 1->N, N->1, or N->N." "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( async def _transfer_one_to_one(
self, self,
sources: Sequence[Container], sources: Sequence[Container],

View File

@@ -52,6 +52,7 @@ from unilabos.devices.liquid_handling.liquid_handler_abstract import (
SimpleReturn, SimpleReturn,
SetLiquidReturn, SetLiquidReturn,
SetLiquidFromPlateReturn, SetLiquidFromPlateReturn,
TransferLiquidReturn,
) )
from unilabos.registry.placeholder_type import ResourceSlot from unilabos.registry.placeholder_type import ResourceSlot
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
@@ -154,25 +155,29 @@ class PRCXI9300Plate(Plate):
**kwargs, **kwargs,
): ):
# 如果 ordered_items 不为 None直接使用 # 如果 ordered_items 不为 None直接使用
items = None
ordering_param = None
if ordered_items is not None: if ordered_items is not None:
items = ordered_items items = ordered_items
elif ordering is not None: elif ordering is not None:
# 检查 ordering 中的值是否是字符串(从 JSON 反序列化时的情况) # 检查 ordering 中的值是否是字符串(从 JSON 反序列化时的情况)
# 如果是字符串,说明这是位置名称,需要让 Plate 自己创建 Well 对象 # 如果是字符串,说明这是位置名称,需要让 Plate 自己创建 Well 对象
# 我们只传递位置信息(键),不传递值,使用 ordering 参数 # 我们只传递位置信息(键),不传递值,使用 ordering 参数
if ordering and isinstance(next(iter(ordering.values()), None), str): if ordering:
# ordering 的值是字符串,只使用键(位置信息)创建新的 OrderedDict values = list(ordering.values())
# 传递 ordering 参数而不是 ordered_items让 Plate 自己创建 Well 对象 value = values[0]
items = None if isinstance(value, str):
# 使用 ordering 参数,只包含位置信息(键) # ordering 的值是字符串,只使用键(位置信息)创建新的 OrderedDict
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys()) # 传递 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: else:
# ordering 的值已经是对象,可以直接使用 # ordering 的值已经是对象,可以直接使用
items = ordering items = ordering
ordering_param = None ordering_param = None
else:
items = None
ordering_param = None
# 根据情况传递不同的参数 # 根据情况传递不同的参数
if items is not None: if items is not None:
@@ -590,7 +595,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
return super().set_liquid(wells, liquid_names, volumes) return super().set_liquid(wells, liquid_names, volumes)
def set_liquid_from_plate( def set_liquid_from_plate(
self, plate: ResourceSlot, well_names: list[str], liquid_names: list[str], volumes: list[float] self, plate: List[ResourceSlot], well_names: list[str], liquid_names: list[str], volumes: list[float]
) -> SetLiquidFromPlateReturn: ) -> SetLiquidFromPlateReturn:
return super().set_liquid_from_plate(plate, well_names, liquid_names, volumes) return super().set_liquid_from_plate(plate, well_names, liquid_names, volumes)
@@ -713,7 +718,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
mix_liquid_height: Optional[float] = None, mix_liquid_height: Optional[float] = None,
delays: Optional[List[int]] = None, delays: Optional[List[int]] = None,
none_keys: List[str] = [], none_keys: List[str] = [],
): ) -> TransferLiquidReturn:
return await super().transfer_liquid( return await super().transfer_liquid(
sources, sources,
targets, targets,

View File

@@ -638,7 +638,7 @@ liquid_handler:
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 吸头迭代函数。用于自动管理和切换吸头架中的吸头,实现批量实验中的吸头自动分配和追踪。该函数监控吸头使用状态,自动切换到下一个可用吸头位置,确保实验流程的连续性。适用于高通量实验、批量处理、自动化流水线等需要大量吸头管理的应用场景。 description: 吸头迭代函数。用于自动管理和切换枪头盒中的吸头,实现批量实验中的吸头自动分配和追踪。该函数监控吸头使用状态,自动切换到下一个可用吸头位置,确保实验流程的连续性。适用于高通量实验、批量处理、自动化流水线等需要大量吸头管理的应用场景。
properties: properties:
feedback: {} feedback: {}
goal: goal:
@@ -712,6 +712,43 @@ liquid_handler:
title: set_group参数 title: set_group参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-set_liquid_from_plate:
feedback: {}
goal: {}
goal_default:
liquid_names: null
plate: null
volumes: null
well_names: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
liquid_names:
type: string
plate:
type: string
volumes:
type: string
well_names:
type: string
required:
- plate
- well_names
- liquid_names
- volumes
type: object
result: {}
required:
- goal
title: set_liquid_from_plate参数
type: object
type: UniLabJsonCommand
auto-set_tiprack: auto-set_tiprack:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -721,7 +758,7 @@ liquid_handler:
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 吸头架设置函数。用于配置和初始化液体处理系统的吸头架信息,包括吸头架位置、类型、容量等参数。该函数建立吸头资源管理系统,为后续的吸头选择和使用提供基础配置。适用于系统初始化、吸头架更换、实验配置等需要吸头资源管理的操作场景。 description: 枪头盒设置函数。用于配置和初始化液体处理系统的枪头盒信息,包括枪头盒位置、类型、容量等参数。该函数建立吸头资源管理系统,为后续的吸头选择和使用提供基础配置。适用于系统初始化、枪头盒更换、实验配置等需要吸头资源管理的操作场景。
properties: properties:
feedback: {} feedback: {}
goal: goal:
@@ -4093,32 +4130,32 @@ liquid_handler:
- 0 - 0
handles: handles:
input: input:
- data_key: liquid - data_key: sources
data_source: handle data_source: handle
data_type: resource data_type: resource
handler_key: sources handler_key: sources
label: sources label: 待移动液体
- data_key: liquid - data_key: targets
data_source: executor
data_type: resource
handler_key: targets
label: targets
- data_key: liquid
data_source: executor
data_type: resource
handler_key: tip_rack
label: tip_rack
output:
- data_key: liquid
data_source: handle data_source: handle
data_type: resource data_type: resource
handler_key: targets
label: 转移目标
- data_key: tip_racks
data_source: handle
data_type: resource
handler_key: tip_rack
label: 枪头盒
output:
- data_key: sources.@flatten
data_source: executor
data_type: resource
handler_key: sources_out handler_key: sources_out
label: sources label: 移液后源孔
- data_key: liquid - data_key: targets.@flatten
data_source: executor data_source: executor
data_type: resource data_type: resource
handler_key: targets_out handler_key: targets_out
label: targets label: 移液后目标孔
placeholder_keys: placeholder_keys:
sources: unilabos_resources sources: unilabos_resources
targets: unilabos_resources targets: unilabos_resources
@@ -5114,19 +5151,34 @@ liquid_handler.biomek:
- 0 - 0
handles: handles:
input: input:
- data_key: liquid - data_key: sources
data_source: handle data_source: handle
data_type: resource data_type: resource
handler_key: liquid-input handler_key: sources
io_type: target io_type: target
label: Liquid Input label: 待移动液体
- data_key: targets
data_source: handle
data_type: resource
handler_key: targets
label: 转移目标
- data_key: tip_racks
data_source: handle
data_type: resource
handler_key: tip_rack
label: 枪头盒
output: output:
- data_key: liquid - data_key: sources.@flatten
data_source: executor data_source: executor
data_type: resource data_type: resource
handler_key: liquid-output handler_key: sources_out
io_type: source io_type: source
label: Liquid Output label: 移液后源孔
- data_key: targets.@flatten
data_source: executor
data_type: resource
handler_key: targets_out
label: 移液后目标孔
placeholder_keys: placeholder_keys:
sources: unilabos_resources sources: unilabos_resources
targets: unilabos_resources targets: unilabos_resources
@@ -9451,78 +9503,81 @@ liquid_handler.prcxi:
type: string type: string
type: array type: array
plate: plate:
properties: items:
category: properties:
type: string category:
children:
items:
type: string type: string
type: array children:
config: items:
type: string type: string
data: type: array
type: string config:
id: type: string
type: string data:
name: type: string
type: string id:
parent: type: string
type: string name:
pose: type: string
properties: parent:
orientation: type: string
properties: pose:
w: properties:
type: number orientation:
x: properties:
type: number w:
y: type: number
type: number x:
z: type: number
type: number y:
required: type: number
- x z:
- y type: number
- z required:
- w - x
title: orientation - y
type: object - z
position: - w
properties: title: orientation
x: type: object
type: number position:
y: properties:
type: number x:
z: type: number
type: number y:
required: type: number
- x z:
- y type: number
- z required:
title: position - x
type: object - y
required: - z
- position title: position
- orientation type: object
title: pose required:
type: object - position
sample_id: - orientation
type: string title: pose
type: type: object
type: string sample_id:
required: type: string
- id type:
- name type: string
- sample_id required:
- children - id
- parent - name
- type - sample_id
- category - children
- pose - parent
- config - type
- data - category
- pose
- config
- data
title: plate
type: object
title: plate title: plate
type: object type: array
volumes: volumes:
items: items:
type: number type: number
@@ -9544,8 +9599,7 @@ liquid_handler.prcxi:
title: Plate title: Plate
type: array type: array
volumes: volumes:
items: items: {}
type: number
title: Volumes title: Volumes
type: array type: array
wells: wells:
@@ -9922,18 +9976,18 @@ liquid_handler.prcxi:
data_source: handle data_source: handle
data_type: resource data_type: resource
handler_key: tip_rack_identifier handler_key: tip_rack_identifier
label: 头盒 label: 头盒
output: output:
- data_key: liquid - data_key: sources.@flatten
data_source: handle data_source: executor
data_type: resource data_type: resource
handler_key: sources_out handler_key: sources_out
label: sources label: 移液后源孔
- data_key: liquid - data_key: targets.@flatten
data_source: executor data_source: executor
data_type: resource data_type: resource
handler_key: targets_out handler_key: targets_out
label: targets label: 移液后目标孔
placeholder_keys: placeholder_keys:
sources: unilabos_resources sources: unilabos_resources
targets: unilabos_resources targets: unilabos_resources

View File

@@ -1581,7 +1581,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
f"转换ResourceSlot列表参数 {arg_name} 失败: {e}\n{traceback.format_exc()}" f"转换ResourceSlot列表参数 {arg_name} 失败: {e}\n{traceback.format_exc()}"
) )
raise JsonCommandInitError(f"ResourceSlot列表参数转换失败: {arg_name}") raise JsonCommandInitError(f"ResourceSlot列表参数转换失败: {arg_name}")
# todo: 默认反报送
return function(**function_args) return function(**function_args)
except KeyError as ex: except KeyError as ex:
raise JsonCommandInitError( raise JsonCommandInitError(
@@ -1614,8 +1614,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
timeout = 30.0 timeout = 30.0
elapsed = 0.0 elapsed = 0.0
while not future.done() and elapsed < timeout: while not future.done() and elapsed < timeout:
time.sleep(0.05) time.sleep(0.02)
elapsed += 0.05 elapsed += 0.02
if not future.done(): if not future.done():
raise Exception(f"资源查询超时: {uuids_list}") raise Exception(f"资源查询超时: {uuids_list}")

View File

@@ -807,7 +807,7 @@ class HostNode(BaseROS2DeviceNode):
assign_sample_id(action_kwargs) assign_sample_id(action_kwargs)
goal_msg = convert_to_ros_msg(action_client._action_type.Goal(), action_kwargs) goal_msg = convert_to_ros_msg(action_client._action_type.Goal(), action_kwargs)
self.lab_logger().info(f"[Host Node] Sending goal for {action_id}: {str(goal_msg)[:1000]}") # self.lab_logger().trace(f"[Host Node] Sending goal for {action_id}: {str(goal_msg)[:1000]}")
self.lab_logger().trace(f"[Host Node] Sending goal for {action_id}: {action_kwargs}") self.lab_logger().trace(f"[Host Node] Sending goal for {action_id}: {action_kwargs}")
self.lab_logger().trace(f"[Host Node] Sending goal for {action_id}: {goal_msg}") self.lab_logger().trace(f"[Host Node] Sending goal for {action_id}: {goal_msg}")
action_client.wait_for_server() action_client.wait_for_server()
@@ -1180,7 +1180,7 @@ class HostNode(BaseROS2DeviceNode):
""" """
更新节点信息回调 更新节点信息回调
""" """
# self.lab_logger().info(f"[Host Node] Node info update request received: {request}") self.lab_logger().trace(f"[Host Node] Node info update request received: {request}")
try: try:
from unilabos.app.communication import get_communication_client from unilabos.app.communication import get_communication_client
from unilabos.app.web.client import HTTPClient, http_client from unilabos.app.web.client import HTTPClient, http_client

View File

@@ -0,0 +1,795 @@
{
"nodes": [
{
"id": "PRCXI",
"name": "PRCXI",
"type": "device",
"class": "liquid_handler.prcxi",
"parent": "",
"pose": {
"size": {
"width": 562,
"height": 394,
"depth": 0
}
},
"config": {
"axis": "Left",
"deck": {
"_resource_type": "unilabos.devices.liquid_handling.prcxi.prcxi:PRCXI9300Deck",
"_resource_child_name": "PRCXI_Deck"
},
"host": "10.20.30.184",
"port": 9999,
"debug": true,
"setup": true,
"is_9320": true,
"timeout": 10,
"matrix_id": "5de524d0-3f95-406c-86dd-f83626ebc7cb",
"simulator": true,
"channel_num": 2
},
"data": {
"reset_ok": true
},
"schema": {},
"description": "",
"model": null,
"position": {
"x": 0,
"y": 240,
"z": 0
}
},
{
"id": "PRCXI_Deck",
"name": "PRCXI_Deck",
"children": [],
"parent": "PRCXI",
"type": "deck",
"class": "",
"position": {
"x": 10,
"y": 10,
"z": 0
},
"config": {
"type": "PRCXI9300Deck",
"size_x": 542,
"size_y": 374,
"size_z": 0,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "deck",
"barcode": null
},
"data": {}
},
{
"id": "T1",
"name": "T1",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 0,
"y": 288,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T1",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
},
{
"id": "T2",
"name": "T2",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 138,
"y": 288,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T2",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
},
{
"id": "T3",
"name": "T3",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 276,
"y": 288,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T3",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
},
{
"id": "T4",
"name": "T4",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 414,
"y": 288,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T4",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
},
{
"id": "T5",
"name": "T5",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 0,
"y": 192,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T5",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
},
{
"id": "T6",
"name": "T6",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 138,
"y": 192,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T6",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
},
{
"id": "T7",
"name": "T7",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 276,
"y": 192,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T7",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
},
{
"id": "T8",
"name": "T8",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 414,
"y": 192,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T8",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
},
{
"id": "T9",
"name": "T9",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 0,
"y": 96,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T9",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
},
{
"id": "T10",
"name": "T10",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 138,
"y": 96,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T10",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
},
{
"id": "T11",
"name": "T11",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 276,
"y": 96,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T11",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
},
{
"id": "T12",
"name": "T12",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 414,
"y": 96,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T12",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
},
{
"id": "T13",
"name": "T13",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T13",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
},
{
"id": "T14",
"name": "T14",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 138,
"y": 0,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T14",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
},
{
"id": "T15",
"name": "T15",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 276,
"y": 0,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T15",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
},
{
"id": "T16",
"name": "T16",
"children": [],
"parent": "PRCXI_Deck",
"type": "plate",
"class": "",
"position": {
"x": 414,
"y": 0,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 127,
"size_y": 85.5,
"size_z": 10,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "plate",
"model": null,
"barcode": null,
"ordering": {},
"sites": [
{
"label": "T16",
"visible": true,
"position": { "x": 0, "y": 0, "z": 0 },
"size": { "width": 128.0, "height": 86, "depth": 0 },
"content_type": [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack"
]
}
]
},
"data": {}
}
],
"edges": []
}

View File

@@ -19,7 +19,9 @@
第一步: 按 slot 去重创建 create_resource 节点(创建板子) 第一步: 按 slot 去重创建 create_resource 节点(创建板子)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
- 首先创建一个 Group 节点type="Group", minimized=true用于包含所有 create_resource 节点
- 遍历所有 reagent按 slot 去重,为每个唯一的 slot 创建一个板子 - 遍历所有 reagent按 slot 去重,为每个唯一的 slot 创建一个板子
- 所有 create_resource 节点的 parent_uuid 指向 Group 节点minimized=true
- 生成参数: - 生成参数:
res_id: plate_slot_{slot} res_id: plate_slot_{slot}
device_id: /PRCXI device_id: /PRCXI
@@ -29,11 +31,13 @@
- 输出端口: labware用于连接 set_liquid_from_plate - 输出端口: labware用于连接 set_liquid_from_plate
- 控制流: create_resource 之间通过 ready 端口串联 - 控制流: create_resource 之间通过 ready 端口串联
示例: slot=1, slot=4 -> 创建 2 个 create_resource 节点 示例: slot=1, slot=4 -> 创建 1 个 Group + 2 个 create_resource 节点
第二步: 为每个 reagent 创建 set_liquid_from_plate 节点(设置液体) 第二步: 为每个 reagent 创建 set_liquid_from_plate 节点(设置液体)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
- 首先创建一个 Group 节点type="Group", minimized=true用于包含所有 set_liquid_from_plate 节点
- 遍历所有 reagent为每个试剂创建 set_liquid_from_plate 节点 - 遍历所有 reagent为每个试剂创建 set_liquid_from_plate 节点
- 所有 set_liquid_from_plate 节点的 parent_uuid 指向 Group 节点minimized=true
- 生成参数: - 生成参数:
plate: [](通过连接传递,来自 create_resource 的 labware plate: [](通过连接传递,来自 create_resource 的 labware
well_names: ["A1", "A3", "A5"](来自 reagent 的 well 数组) well_names: ["A1", "A3", "A5"](来自 reagent 的 well 数组)
@@ -76,6 +80,13 @@ transfer_liquid:
输入: sources -> sources_identifier, targets -> targets_identifier 输入: sources -> sources_identifier, targets -> targets_identifier
输出: sources -> sources_out, targets -> targets_out 输出: sources -> sources_out, targets -> targets_out
==================== 设备名配置 (device_name) ====================
每个节点都有 device_name 字段,指定在哪个设备上执行:
- create_resource: device_name = "host_node"(固定)
- set_liquid_from_plate: device_name = "PRCXI"(可配置,见 DEVICE_NAME_DEFAULT
- transfer_liquid 等动作: device_name = "PRCXI"(可配置,见 DEVICE_NAME_DEFAULT
==================== 校验规则 ==================== ==================== 校验规则 ====================
- 检查 sources/targets 是否在 reagent 中定义 - 检查 sources/targets 是否在 reagent 中定义
@@ -97,6 +108,13 @@ Json = Dict[str, Any]
# ==================== 默认配置 ==================== # ==================== 默认配置 ====================
# 设备名配置
DEVICE_NAME_HOST = "host_node" # create_resource 固定在 host_node 上执行
DEVICE_NAME_DEFAULT = "PRCXI" # transfer_liquid, set_liquid_from_plate 等动作的默认设备名
# 节点类型
NODE_TYPE_DEFAULT = "ILab" # 所有节点的默认类型
# create_resource 节点默认参数 # create_resource 节点默认参数
CREATE_RESOURCE_DEFAULTS = { CREATE_RESOURCE_DEFAULTS = {
"device_id": "/PRCXI", "device_id": "/PRCXI",
@@ -367,6 +385,21 @@ def build_protocol_graph(
"res_id": res_id, "res_id": res_id,
} }
# 创建 Group 节点,包含所有 create_resource 节点
group_node_id = str(uuid.uuid4())
G.add_node(
group_node_id,
name="Resources Group",
type="Group",
parent_uuid="",
lab_node_type="Device",
template_name="",
resource_name="",
footer="",
minimized=True,
param=None,
)
# 为每个唯一的 slot 创建 create_resource 节点 # 为每个唯一的 slot 创建 create_resource 节点
res_index = 0 res_index = 0
last_create_resource_id = None last_create_resource_id = None
@@ -383,6 +416,10 @@ def build_protocol_graph(
description=f"Create plate on slot {slot}", description=f"Create plate on slot {slot}",
lab_node_type="Labware", lab_node_type="Labware",
footer="create_resource-host_node", footer="create_resource-host_node",
device_name=DEVICE_NAME_HOST,
type=NODE_TYPE_DEFAULT,
parent_uuid=group_node_id, # 指向 Group 节点
minimized=True, # 折叠显示
param={ param={
"res_id": res_id, "res_id": res_id,
"device_id": CREATE_RESOURCE_DEFAULTS["device_id"], "device_id": CREATE_RESOURCE_DEFAULTS["device_id"],
@@ -400,6 +437,21 @@ def build_protocol_graph(
last_create_resource_id = node_id last_create_resource_id = node_id
# ==================== 第二步:为每个 reagent 创建 set_liquid_from_plate 节点 ==================== # ==================== 第二步:为每个 reagent 创建 set_liquid_from_plate 节点 ====================
# 创建 Group 节点,包含所有 set_liquid_from_plate 节点
set_liquid_group_id = str(uuid.uuid4())
G.add_node(
set_liquid_group_id,
name="SetLiquid Group",
type="Group",
parent_uuid="",
lab_node_type="Device",
template_name="",
resource_name="",
footer="",
minimized=True,
param=None,
)
set_liquid_index = 0 set_liquid_index = 0
last_set_liquid_id = last_create_resource_id # set_liquid_from_plate 连接在 create_resource 之后 last_set_liquid_id = last_create_resource_id # set_liquid_from_plate 连接在 create_resource 之后
@@ -430,6 +482,10 @@ def build_protocol_graph(
description=f"Set liquid: {labware_id}", description=f"Set liquid: {labware_id}",
lab_node_type="Reagent", lab_node_type="Reagent",
footer="set_liquid_from_plate-liquid_handler.prcxi", footer="set_liquid_from_plate-liquid_handler.prcxi",
device_name=DEVICE_NAME_DEFAULT,
type=NODE_TYPE_DEFAULT,
parent_uuid=set_liquid_group_id, # 指向 Group 节点
minimized=True, # 折叠显示
param={ param={
"plate": [], # 通过连接传递 "plate": [], # 通过连接传递
"well_names": wells, # 孔位名数组,如 ["A1", "A3", "A5"] "well_names": wells, # 孔位名数组,如 ["A1", "A3", "A5"]
@@ -544,9 +600,11 @@ def build_protocol_graph(
if param_key in params: if param_key in params:
params[param_key] = [] params[param_key] = []
# 更新 step 的 paramfooter # 更新 step 的 paramfooter、device_name 和 type
step_copy = step.copy() step_copy = step.copy()
step_copy["param"] = params step_copy["param"] = params
step_copy["device_name"] = DEVICE_NAME_DEFAULT # 动作节点使用默认设备名
step_copy["type"] = NODE_TYPE_DEFAULT # 节点类型
# 如果有警告,修改 footer 添加警告标记(警告放前面) # 如果有警告,修改 footer 添加警告标记(警告放前面)
if warnings: if warnings: