diff --git a/.conda/base/recipe.yaml b/.conda/base/recipe.yaml index 3c5bb88..fd20564 100644 --- a/.conda/base/recipe.yaml +++ b/.conda/base/recipe.yaml @@ -46,7 +46,7 @@ requirements: - jinja2 - requests - uvicorn - - opcua + - opcua # [not osx] - pyserial - pandas - pymodbus diff --git a/unilabos/devices/liquid_handling/liquid_handler_abstract.py b/unilabos/devices/liquid_handling/liquid_handler_abstract.py index aa695a0..24d8518 100644 --- a/unilabos/devices/liquid_handling/liquid_handler_abstract.py +++ b/unilabos/devices/liquid_handling/liquid_handler_abstract.py @@ -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 ( @@ -28,14 +24,27 @@ from pylabrobot.resources import ( Tip, ) -from unilabos.registry.placeholder_type import ResourceSlot from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode -from unilabos.resources.resource_tracker import ResourceTreeSet - - class SimpleReturn(TypedDict): - samples: list - volumes: list + samples: List[List[ResourceDict]] + volumes: List[float] + + +class SetLiquidReturn(TypedDict): + wells: List[List[ResourceDict]] + volumes: List[float] + + +class SetLiquidFromPlateReturn(TypedDict): + plate: List[List[ResourceDict]] + wells: List[List[ResourceDict]] + volumes: List[float] + + +class TransferLiquidReturn(TypedDict): + sources: List[List[ResourceDict]] + targets: List[List[ResourceDict]] + class SetLiquidReturn(TypedDict): @@ -678,40 +687,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware): well.set_liquids([(liquid_name, volume)]) # type: ignore res_volumes.append(volume) - return SetLiquidReturn( - 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] - ) -> 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。 - """ - # 根据 well_names 获取对应的 Well 对象 - wells = [plate.get_well(name) for name in well_names] - res_volumes = [] - - # 如果 liquid_names 和 volumes 都为空,直接返回 - if not liquid_names and not volumes: - 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 - volumes=res_volumes, - ) - - for well, liquid_name, volume in zip(wells, liquid_names, volumes): - well.set_liquids([(liquid_name, volume)]) # type: ignore - res_volumes.append(volume) - - 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 - volumes=res_volumes, - ) - + return SimpleReturn(samples=res_samples, volumes=res_volumes) # --------------------------------------------------------------- # REMOVE LIQUID -------------------------------------------------- # --------------------------------------------------------------- @@ -1111,7 +1087,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 +1227,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], diff --git a/unilabos/devices/liquid_handling/prcxi/prcxi.py b/unilabos/devices/liquid_handling/prcxi/prcxi.py index 4f96255..f337529 100644 --- a/unilabos/devices/liquid_handling/prcxi/prcxi.py +++ b/unilabos/devices/liquid_handling/prcxi/prcxi.py @@ -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 @@ -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: @@ -590,7 +595,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract): return super().set_liquid(wells, liquid_names, volumes) 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: 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, delays: Optional[List[int]] = None, none_keys: List[str] = [], - ): + ) -> TransferLiquidReturn: return await super().transfer_liquid( sources, targets, diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index b2612e7..319d033 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -638,7 +638,7 @@ liquid_handler: placeholder_keys: {} result: {} schema: - description: 吸头迭代函数。用于自动管理和切换吸头架中的吸头,实现批量实验中的吸头自动分配和追踪。该函数监控吸头使用状态,自动切换到下一个可用吸头位置,确保实验流程的连续性。适用于高通量实验、批量处理、自动化流水线等需要大量吸头管理的应用场景。 + description: 吸头迭代函数。用于自动管理和切换枪头盒中的吸头,实现批量实验中的吸头自动分配和追踪。该函数监控吸头使用状态,自动切换到下一个可用吸头位置,确保实验流程的连续性。适用于高通量实验、批量处理、自动化流水线等需要大量吸头管理的应用场景。 properties: feedback: {} goal: @@ -712,6 +712,43 @@ liquid_handler: title: set_group参数 type: object 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: feedback: {} goal: {} @@ -721,7 +758,7 @@ liquid_handler: placeholder_keys: {} result: {} schema: - description: 吸头架设置函数。用于配置和初始化液体处理系统的吸头架信息,包括吸头架位置、类型、容量等参数。该函数建立吸头资源管理系统,为后续的吸头选择和使用提供基础配置。适用于系统初始化、吸头架更换、实验配置等需要吸头资源管理的操作场景。 + description: 枪头盒设置函数。用于配置和初始化液体处理系统的枪头盒信息,包括枪头盒位置、类型、容量等参数。该函数建立吸头资源管理系统,为后续的吸头选择和使用提供基础配置。适用于系统初始化、枪头盒更换、实验配置等需要吸头资源管理的操作场景。 properties: feedback: {} goal: @@ -4093,32 +4130,32 @@ liquid_handler: - 0 handles: input: - - data_key: liquid + - data_key: sources data_source: handle data_type: resource handler_key: sources - label: sources - - data_key: liquid - 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 + 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: + - data_key: sources.@flatten + data_source: executor + data_type: resource handler_key: sources_out - label: sources - - data_key: liquid + label: 移液后源孔 + - data_key: targets.@flatten data_source: executor data_type: resource handler_key: targets_out - label: targets + label: 移液后目标孔 placeholder_keys: sources: unilabos_resources targets: unilabos_resources @@ -5114,19 +5151,34 @@ liquid_handler.biomek: - 0 handles: input: - - data_key: liquid + - data_key: sources data_source: handle data_type: resource - handler_key: liquid-input + handler_key: sources 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: - - data_key: liquid + - data_key: sources.@flatten data_source: executor data_type: resource - handler_key: liquid-output + handler_key: sources_out io_type: source - label: Liquid Output + label: 移液后源孔 + - data_key: targets.@flatten + data_source: executor + data_type: resource + handler_key: targets_out + label: 移液后目标孔 placeholder_keys: sources: unilabos_resources targets: unilabos_resources @@ -9451,78 +9503,81 @@ liquid_handler.prcxi: type: string type: array plate: - properties: - category: - type: string - children: - items: + items: + properties: + category: type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: plate + type: object title: plate - type: object + type: array volumes: items: type: number @@ -9544,8 +9599,7 @@ liquid_handler.prcxi: title: Plate type: array volumes: - items: - type: number + items: {} title: Volumes type: array wells: @@ -9922,18 +9976,18 @@ liquid_handler.prcxi: data_source: handle data_type: resource handler_key: tip_rack_identifier - label: 墙头盒 + label: 枪头盒 output: - - data_key: liquid - data_source: handle + - data_key: sources.@flatten + data_source: executor data_type: resource handler_key: sources_out - label: sources - - data_key: liquid + label: 移液后源孔 + - data_key: targets.@flatten data_source: executor data_type: resource handler_key: targets_out - label: targets + label: 移液后目标孔 placeholder_keys: sources: unilabos_resources targets: unilabos_resources diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 8d0c4e0..cae23a7 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -151,12 +151,40 @@ def canonicalize_links_ports(links: List[Dict[str, Any]], resource_tree_set: Res """ # 构建 id 到 uuid 的映射 id_to_uuid: Dict[str, str] = {} + uuid_to_id: Dict[str, str] = {} for node in resource_tree_set.all_nodes: id_to_uuid[node.res_content.id] = node.res_content.uuid + uuid_to_id[node.res_content.uuid] = node.res_content.id + + # 第三遍处理:为每个 link 添加 source_uuid 和 target_uuid + for link in links: + source_id = link.get("source") + target_id = link.get("target") + + # 添加 source_uuid + if source_id and source_id in id_to_uuid: + link["source_uuid"] = id_to_uuid[source_id] + + # 添加 target_uuid + if target_id and target_id in id_to_uuid: + link["target_uuid"] = id_to_uuid[target_id] + + source_uuid = link.get("source_uuid") + target_uuid = link.get("target_uuid") + + # 添加 source_uuid + if source_uuid and source_uuid in uuid_to_id: + link["source"] = uuid_to_id[source_uuid] + + # 添加 target_uuid + if target_uuid and target_uuid in uuid_to_id: + link["target"] = uuid_to_id[target_uuid] # 第一遍处理:将字符串类型的port转换为字典格式 for link in links: port = link.get("port") + if port is None: + continue if link.get("type", "physical") == "physical": link["type"] = "fluid" if isinstance(port, int): @@ -179,13 +207,15 @@ def canonicalize_links_ports(links: List[Dict[str, Any]], resource_tree_set: Res link["port"] = {link["source"]: None, link["target"]: None} # 构建边字典,键为(source节点, target节点),值为对应的port信息 - edges = {(link["source"], link["target"]): link["port"] for link in links} + edges = {(link["source"], link["target"]): link["port"] for link in links if link.get("port")} # 第二遍处理:填充反向边的dest信息 delete_reverses = [] for i, link in enumerate(links): s, t = link["source"], link["target"] - current_port = link["port"] + current_port = link.get("port") + if current_port is None: + continue if current_port.get(t) is None: reverse_key = (t, s) reverse_port = edges.get(reverse_key) @@ -200,20 +230,6 @@ def canonicalize_links_ports(links: List[Dict[str, Any]], resource_tree_set: Res current_port[t] = current_port[s] # 删除已被使用反向端口信息的反向边 standardized_links = [link for i, link in enumerate(links) if i not in delete_reverses] - - # 第三遍处理:为每个 link 添加 source_uuid 和 target_uuid - for link in standardized_links: - source_id = link.get("source") - target_id = link.get("target") - - # 添加 source_uuid - if source_id and source_id in id_to_uuid: - link["source_uuid"] = id_to_uuid[source_id] - - # 添加 target_uuid - if target_id and target_id in id_to_uuid: - link["target_uuid"] = id_to_uuid[target_id] - return standardized_links @@ -284,6 +300,8 @@ def modify_to_backend_format(data: list[dict[str, Any]]) -> list[dict[str, Any]] edge["sourceHandle"] = port[source] elif "source_port" in edge: edge["sourceHandle"] = edge.pop("source_port") + elif "source_handle" in edge: + edge["sourceHandle"] = edge.pop("source_handle") else: typ = edge.get("type") if typ == "communication": @@ -292,6 +310,8 @@ def modify_to_backend_format(data: list[dict[str, Any]]) -> list[dict[str, Any]] edge["targetHandle"] = port[target] elif "target_port" in edge: edge["targetHandle"] = edge.pop("target_port") + elif "target_handle" in edge: + edge["targetHandle"] = edge.pop("target_handle") else: typ = edge.get("type") if typ == "communication": diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 3d1ffda..56585f6 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -1581,7 +1581,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): f"转换ResourceSlot列表参数 {arg_name} 失败: {e}\n{traceback.format_exc()}" ) raise JsonCommandInitError(f"ResourceSlot列表参数转换失败: {arg_name}") - + # todo: 默认反报送 return function(**function_args) except KeyError as ex: raise JsonCommandInitError( @@ -1614,8 +1614,8 @@ class BaseROS2DeviceNode(Node, Generic[T]): timeout = 30.0 elapsed = 0.0 while not future.done() and elapsed < timeout: - time.sleep(0.05) - elapsed += 0.05 + time.sleep(0.02) + elapsed += 0.02 if not future.done(): raise Exception(f"资源查询超时: {uuids_list}") diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index e95b393..4cfa9c1 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -807,7 +807,7 @@ class HostNode(BaseROS2DeviceNode): assign_sample_id(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}: {goal_msg}") 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: from unilabos.app.communication import get_communication_client from unilabos.app.web.client import HTTPClient, http_client diff --git a/unilabos/test/experiments/prcxi_9320_slim.json b/unilabos/test/experiments/prcxi_9320_slim.json new file mode 100644 index 0000000..85bc90c --- /dev/null +++ b/unilabos/test/experiments/prcxi_9320_slim.json @@ -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": [] +} diff --git a/unilabos/workflow/common.py b/unilabos/workflow/common.py index ad073d9..f4c0ac8 100644 --- a/unilabos/workflow/common.py +++ b/unilabos/workflow/common.py @@ -19,7 +19,9 @@ 第一步: 按 slot 去重创建 create_resource 节点(创建板子) -------------------------------------------------------------------------------- +- 首先创建一个 Group 节点(type="Group", minimized=true),用于包含所有 create_resource 节点 - 遍历所有 reagent,按 slot 去重,为每个唯一的 slot 创建一个板子 +- 所有 create_resource 节点的 parent_uuid 指向 Group 节点,minimized=true - 生成参数: res_id: plate_slot_{slot} device_id: /PRCXI @@ -29,11 +31,13 @@ - 输出端口: labware(用于连接 set_liquid_from_plate) - 控制流: create_resource 之间通过 ready 端口串联 -示例: slot=1, slot=4 -> 创建 2 个 create_resource 节点 +示例: slot=1, slot=4 -> 创建 1 个 Group + 2 个 create_resource 节点 第二步: 为每个 reagent 创建 set_liquid_from_plate 节点(设置液体) -------------------------------------------------------------------------------- +- 首先创建一个 Group 节点(type="Group", minimized=true),用于包含所有 set_liquid_from_plate 节点 - 遍历所有 reagent,为每个试剂创建 set_liquid_from_plate 节点 +- 所有 set_liquid_from_plate 节点的 parent_uuid 指向 Group 节点,minimized=true - 生成参数: plate: [](通过连接传递,来自 create_resource 的 labware) well_names: ["A1", "A3", "A5"](来自 reagent 的 well 数组) @@ -76,6 +80,13 @@ transfer_liquid: 输入: sources -> sources_identifier, targets -> targets_identifier 输出: 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 中定义 @@ -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_DEFAULTS = { "device_id": "/PRCXI", @@ -367,6 +385,21 @@ def build_protocol_graph( "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 节点 res_index = 0 last_create_resource_id = None @@ -383,6 +416,10 @@ def build_protocol_graph( description=f"Create plate on slot {slot}", lab_node_type="Labware", footer="create_resource-host_node", + device_name=DEVICE_NAME_HOST, + type=NODE_TYPE_DEFAULT, + parent_uuid=group_node_id, # 指向 Group 节点 + minimized=True, # 折叠显示 param={ "res_id": res_id, "device_id": CREATE_RESOURCE_DEFAULTS["device_id"], @@ -400,6 +437,21 @@ def build_protocol_graph( last_create_resource_id = node_id # ==================== 第二步:为每个 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 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}", lab_node_type="Reagent", 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={ "plate": [], # 通过连接传递 "well_names": wells, # 孔位名数组,如 ["A1", "A3", "A5"] @@ -544,9 +600,11 @@ def build_protocol_graph( if param_key in params: params[param_key] = [] - # 更新 step 的 param 和 footer + # 更新 step 的 param、footer、device_name 和 type step_copy = step.copy() step_copy["param"] = params + step_copy["device_name"] = DEVICE_NAME_DEFAULT # 动作节点使用默认设备名 + step_copy["type"] = NODE_TYPE_DEFAULT # 节点类型 # 如果有警告,修改 footer 添加警告标记(警告放前面) if warnings: