From e1074f06d2e6e6459419ffd595d8d06d729f05ab Mon Sep 17 00:00:00 2001 From: q434343 <554662886@qq.com> Date: Thu, 26 Feb 2026 10:52:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=B7=A5=E4=BD=9C=E6=B5=81?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E4=BB=A5=E5=8F=8Alh=E7=9A=84=E7=89=A9?= =?UTF-8?q?=E6=96=99=E5=88=9D=E6=AD=A5=E5=88=A4=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../liquid_handler_abstract.py | 10 +-- unilabos/resources/resource_tracker.py | 69 ++++++++++++++++++- unilabos/workflow/common.py | 13 ++-- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/unilabos/devices/liquid_handling/liquid_handler_abstract.py b/unilabos/devices/liquid_handling/liquid_handler_abstract.py index ef69f8dc..811563a1 100644 --- a/unilabos/devices/liquid_handling/liquid_handler_abstract.py +++ b/unilabos/devices/liquid_handling/liquid_handler_abstract.py @@ -1208,11 +1208,11 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware): len_asp_vols = len(asp_vols) len_dis_vols = len(dis_vols) - if num_targets != 1 and num_sources != 1: - if len_asp_vols != num_sources and len_asp_vols != num_targets: - raise ValueError(f"asp_vols length must be equal to sources or targets length, but got {len_asp_vols} and {num_sources} and {num_targets}") - if len_dis_vols != num_sources and len_dis_vols != num_targets: - raise ValueError(f"dis_vols length must be equal to sources or targets length, but got {len_dis_vols} and {num_sources} and {num_targets}") + # if num_targets != 1 and num_sources != 1: + # if len_asp_vols != num_sources and len_asp_vols != num_targets: + # raise ValueError(f"asp_vols length must be equal to sources or targets length, but got {len_asp_vols} and {num_sources} and {num_targets}") + # if len_dis_vols != num_sources and len_dis_vols != num_targets: + # raise ValueError(f"dis_vols length must be equal to sources or targets length, but got {len_dis_vols} and {num_sources} and {num_targets}") if len(use_channels) == 1: max_len = max(num_sources, num_targets) diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index e042ef80..be036126 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -572,6 +572,71 @@ class ResourceTreeSet(object): d["model"] = res.config.get("model", None) return d + # deserialize 会单独处理的元数据 key,不传给构造函数 + _META_KEYS = {"type", "parent_name", "location", "children", "rotation", "barcode"} + # deserialize 自定义逻辑使用的 key(如 TipSpot 用 prototype_tip 构建 make_tip),需保留 + _DESERIALIZE_PRESERVED_KEYS = {"prototype_tip"} + + def remove_incompatible_params(plr_d: dict) -> None: + """递归移除 PLR 类不接受的参数,避免 deserialize 报错。 + - 移除构造函数不接受的参数(如 compute_height_from_volume、ordering、category) + - 对 TubeRack:将 ordering 转为 ordered_items + - 保留 deserialize 自定义逻辑需要的 key(如 prototype_tip) + """ + if "type" in plr_d: + sub_cls = find_subclass(plr_d["type"], PLRResource) + if sub_cls is not None: + spec = inspect.signature(sub_cls) + valid_params = set(spec.parameters.keys()) + # TubeRack 特殊处理:先转换 ordering,再参与后续过滤 + if "ordering" not in valid_params and "ordering" in plr_d: + ordering = plr_d.pop("ordering", None) + if sub_cls.__name__ == "TubeRack": + plr_d["ordered_items"] = ( + _ordering_to_ordered_items(plr_d, ordering) + if ordering + else {} + ) + # 移除构造函数不接受的参数(保留 META 和 deserialize 自定义逻辑需要的 key) + for key in list(plr_d.keys()): + if ( + key not in _META_KEYS + and key not in _DESERIALIZE_PRESERVED_KEYS + and key not in valid_params + ): + plr_d.pop(key, None) + for child in plr_d.get("children", []): + remove_incompatible_params(child) + + def _ordering_to_ordered_items(plr_d: dict, ordering: dict) -> dict: + """将 ordering 转为 ordered_items,从 children 构建 Tube 对象""" + from pylabrobot.resources import Tube, Coordinate + from pylabrobot.serializer import deserialize as plr_deserialize + + children = plr_d.get("children", []) + ordered_items = {} + for idx, (ident, child_name) in enumerate(ordering.items()): + child_data = children[idx] if idx < len(children) else None + if child_data is None: + continue + loc_data = child_data.get("location") + loc = ( + plr_deserialize(loc_data) + if loc_data + else Coordinate(0, 0, 0) + ) + tube = Tube( + name=child_data.get("name", child_name or ident), + size_x=child_data.get("size_x", 10), + size_y=child_data.get("size_y", 10), + size_z=child_data.get("size_z", 50), + max_volume=child_data.get("max_volume", 1000), + ) + tube.location = loc + ordered_items[ident] = tube + plr_d["children"] = [] # 已并入 ordered_items,避免重复反序列化 + return ordered_items + plr_resources = [] tracker = DeviceNodeResourceTracker() @@ -591,9 +656,7 @@ class ResourceTreeSet(object): raise ValueError( f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}" ) - spec = inspect.signature(sub_cls) - if "category" not in spec.parameters: - plr_dict.pop("category", None) + remove_incompatible_params(plr_dict) plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True) from pylabrobot.resources import Coordinate from pylabrobot.serializer import deserialize diff --git a/unilabos/workflow/common.py b/unilabos/workflow/common.py index d9038250..c913d9c4 100644 --- a/unilabos/workflow/common.py +++ b/unilabos/workflow/common.py @@ -413,17 +413,14 @@ def build_protocol_graph( for slot, info in slots_info.items(): node_id = str(uuid.uuid4()) res_id = info["res_id"] - res_type_name = info["labware"] - if "tip" in res_type_name.lower(): - res_type = "tip_rack" - else: - res_type = "plate" + res_type_name = info["labware"].lower().replace(".", "point") + res_type_name = f"lab_{res_type_name}" G.add_node( node_id, template_name="create_resource", resource_name="host_node", - name=f"{res_type} {slot}", + name=f"{res_type_name}_slot{slot}", description=f"Create plate on slot {slot}", lab_node_type="Labware", footer="create_resource-host_node", @@ -434,14 +431,14 @@ def build_protocol_graph( param={ "res_id": res_id, "device_id": CREATE_RESOURCE_DEFAULTS["device_id"], - "class_name": CLASS_NAMES_MAPPING[res_type], + "class_name": res_type_name, "parent": CREATE_RESOURCE_DEFAULTS["parent_template"].format(slot=slot), "bind_locations": {"x": 0.0, "y": 0.0, "z": 0.0}, "slot_on_deck": slot, }, ) slot_to_create_resource[slot] = node_id - if res_type == "tip_rack": + if "tip" in res_type_name and "rack" in res_type_name: resource_last_writer[info["labware_id"]] = f"{node_id}:labware" # create_resource 之间不需要 ready 连接