diff --git a/scripts/workflow.py b/scripts/workflow.py index 6db3c13e..be7bbd1e 100644 --- a/scripts/workflow.py +++ b/scripts/workflow.py @@ -92,7 +92,7 @@ def refactor_data(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]: # 定义操作映射,包含生物实验和有机化学的所有操作 OPERATION_MAPPING = { # 生物实验操作 - "transfer_liquid": "SynBioFactory-liquid_handler.transfer_liquid", + "transfer_liquid": "SynBioFactory-liquid_handler.prcxi-transfer_liquid", "transfer": "SynBioFactory-liquid_handler.biomek-transfer", "incubation": "SynBioFactory-liquid_handler.biomek-incubation", "move_labware": "SynBioFactory-liquid_handler.biomek-move_labware", @@ -159,7 +159,10 @@ def build_protocol_graph( protocol_steps = refactor_data(protocol_steps) # 检查协议步骤中的模板来判断协议类型 - has_biomek_template = any("biomek" in step.get("template", "") for step in protocol_steps) + has_biomek_template = any( + ("biomek" in step.get("template", "")) or ("prcxi" in step.get("template", "")) + for step in protocol_steps + ) if has_biomek_template: # 生物实验协议图构建 @@ -178,12 +181,14 @@ def build_protocol_graph( resource_last_writer[labware_id] = f"{node_id}:labware" # 处理协议步骤 + prev_node = None for i, step in enumerate(protocol_steps): node_id = str(uuid.uuid4()) G.add_node(node_id, **step) # 添加控制流边 - G.add_edge(prev_node, node_id, source_port="ready", target_port="ready") + if prev_node is not None: + G.add_edge(prev_node, node_id, source_port="ready", target_port="ready") prev_node = node_id # 处理物料流 @@ -198,7 +203,8 @@ def build_protocol_graph( # 添加协议结束节点 end_id = str(uuid.uuid4()) G.add_node(end_id, template=f"{LAB_NAME}-liquid_handler.biomek-run_protocol") - G.add_edge(prev_node, end_id, source_port="ready", target_port="ready") + if prev_node is not None: + G.add_edge(prev_node, end_id, source_port="ready", target_port="ready") else: # 有机化学协议图构建 diff --git a/test/workflow/example_bio_graph.png b/test/workflow/example_bio_graph.png new file mode 100644 index 00000000..351cceb8 Binary files /dev/null and b/test/workflow/example_bio_graph.png differ diff --git a/test/workflow/example_prcxi.json b/test/workflow/example_prcxi.json new file mode 100644 index 00000000..d9abd3d8 --- /dev/null +++ b/test/workflow/example_prcxi.json @@ -0,0 +1,63 @@ +{ + "steps_info": [ + { + "step_number": 1, + "action": "transfer_liquid", + "parameters": { + "source": "sample supernatant", + "target": "antibody-coated well", + "volume": 100 + } + }, + { + "step_number": 2, + "action": "transfer_liquid", + "parameters": { + "source": "washing buffer", + "target": "antibody-coated well", + "volume": 200 + } + }, + { + "step_number": 3, + "action": "transfer_liquid", + "parameters": { + "source": "washing buffer", + "target": "antibody-coated well", + "volume": 200 + } + }, + { + "step_number": 4, + "action": "transfer_liquid", + "parameters": { + "source": "washing buffer", + "target": "antibody-coated well", + "volume": 200 + } + }, + { + "step_number": 5, + "action": "transfer_liquid", + "parameters": { + "source": "TMB substrate", + "target": "antibody-coated well", + "volume": 100 + } + } + ], + "labware_info": [ + {"reagent_name": "sample supernatant", "material_name": "96深孔板", "positions": 1}, + {"reagent_name": "washing buffer", "material_name": "储液槽", "positions": 2}, + {"reagent_name": "TMB substrate", "material_name": "储液槽", "positions": 3}, + {"reagent_name": "antibody-coated well", "material_name": "96 细胞培养皿", "positions": 4}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 5}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 6}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 7}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 8}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 9}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 10}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 11}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 13} + ] +} \ No newline at end of file diff --git a/test/workflow/example_prcxi_graph.png b/test/workflow/example_prcxi_graph.png new file mode 100644 index 00000000..96cecdb5 Binary files /dev/null and b/test/workflow/example_prcxi_graph.png differ diff --git a/test/workflow/example_prcxi_graph_20251022_1359.png b/test/workflow/example_prcxi_graph_20251022_1359.png new file mode 100644 index 00000000..7cf4f7e7 Binary files /dev/null and b/test/workflow/example_prcxi_graph_20251022_1359.png differ diff --git a/test/workflow/merge_workflow.py b/test/workflow/merge_workflow.py index f42783c5..fb409769 100644 --- a/test/workflow/merge_workflow.py +++ b/test/workflow/merge_workflow.py @@ -1,14 +1,94 @@ -import pytest import json +import sys +from datetime import datetime +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[2] +if str(ROOT_DIR) not in sys.path: + sys.path.insert(0, str(ROOT_DIR)) + +import pytest + from scripts.workflow import build_protocol_graph, draw_protocol_graph, draw_protocol_graph_with_ports +ROOT_DIR = Path(__file__).resolve().parents[2] +if str(ROOT_DIR) not in sys.path: + sys.path.insert(0, str(ROOT_DIR)) + + +def _normalize_steps(data): + normalized = [] + for step in data: + action = step.get("action") or step.get("operation") + if not action: + continue + raw_params = step.get("parameters") or step.get("action_args") or {} + params = dict(raw_params) + + if "source" in raw_params and "sources" not in raw_params: + params["sources"] = raw_params["source"] + if "target" in raw_params and "targets" not in raw_params: + params["targets"] = raw_params["target"] + + description = step.get("description") or step.get("purpose") + step_dict = {"action": action, "parameters": params} + if description: + step_dict["description"] = description + normalized.append(step_dict) + return normalized + + +def _normalize_labware(data): + labware = {} + for item in data: + reagent_name = item.get("reagent_name") + key = reagent_name or item.get("material_name") or item.get("name") + if not key: + continue + key = str(key) + idx = 1 + original_key = key + while key in labware: + idx += 1 + key = f"{original_key}_{idx}" + + labware[key] = { + "slot": item.get("positions") or item.get("slot"), + "labware": item.get("material_name") or item.get("labware"), + "well": item.get("well", []), + "type": item.get("type", "reagent"), + "role": item.get("role", ""), + "name": key, + } + return labware + + @pytest.mark.parametrize("protocol_name", [ "example_bio", # "bioyond_materials_liquidhandling_1", + "example_prcxi", ]) def test_build_protocol_graph(protocol_name): - d = json.load(open(f"{protocol_name}.json")) - graph = build_protocol_graph(labware_info=d["reagent"], protocol_steps=d["workflow"], workstation_name="PRCXi") - draw_protocol_graph_with_ports(graph, "graph.png") + data_path = Path(__file__).with_name(f"{protocol_name}.json") + with data_path.open("r", encoding="utf-8") as fp: + d = json.load(fp) + + if "workflow" in d and "reagent" in d: + protocol_steps = d["workflow"] + labware_info = d["reagent"] + elif "steps_info" in d and "labware_info" in d: + protocol_steps = _normalize_steps(d["steps_info"]) + labware_info = _normalize_labware(d["labware_info"]) + else: + raise ValueError("Unsupported protocol format") + + graph = build_protocol_graph( + labware_info=labware_info, + protocol_steps=protocol_steps, + workstation_name="PRCXi", + ) + timestamp = datetime.now().strftime("%Y%m%d_%H%M") + output_path = data_path.with_name(f"{protocol_name}_graph_{timestamp}.png") + draw_protocol_graph_with_ports(graph, str(output_path)) print(graph) \ No newline at end of file