mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-15 13:44:39 +00:00
Update workstation & bioyond example
Refine descriptions in Bioyond reaction station YAML Updated and clarified field and operation descriptions in the reaction_station_bioyond.yaml file for improved accuracy and consistency. Changes include more precise terminology, clearer parameter explanations, and standardized formatting for operation schemas. refactor(workstation): 更新反应站参数描述并添加分液站配置文件 修正反应站方法参数描述,使其更准确清晰 添加bioyond_dispensing_station.yaml配置文件 add create_workflow script and test add invisible_slots to carriers fix(warehouses): 修正bioyond_warehouse_1x4x4仓库的尺寸参数 调整仓库的num_items_x和num_items_z值以匹配实际布局,并更新物品尺寸参数 save resource get data. allow empty value for layout and cross_section_type More decks&plates support for bioyond (#115) refactor(registry): 重构反应站设备配置,简化并更新操作命令 移除旧的自动操作命令,新增针对具体化学操作的命令配置 更新模块路径和配置结构,优化参数定义和描述 fix(dispensing_station): 修正物料信息查询方法调用 将直接调用material_id_query改为通过hardware_interface调用,以符合接口设计规范
This commit is contained in:
689
scripts/workflow.py
Normal file
689
scripts/workflow.py
Normal file
@@ -0,0 +1,689 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
import networkx as nx
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import requests
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleGraph:
|
||||||
|
"""简单的有向图实现,用于构建工作流图"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.nodes = {}
|
||||||
|
self.edges = []
|
||||||
|
|
||||||
|
def add_node(self, node_id, **attrs):
|
||||||
|
"""添加节点"""
|
||||||
|
self.nodes[node_id] = attrs
|
||||||
|
|
||||||
|
def add_edge(self, source, target, **attrs):
|
||||||
|
"""添加边"""
|
||||||
|
edge = {"source": source, "target": target, **attrs}
|
||||||
|
self.edges.append(edge)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""转换为工作流图格式"""
|
||||||
|
nodes_list = []
|
||||||
|
for node_id, attrs in self.nodes.items():
|
||||||
|
node_attrs = attrs.copy()
|
||||||
|
params = node_attrs.pop("parameters", {}) or {}
|
||||||
|
node_attrs.update(params)
|
||||||
|
nodes_list.append({"id": node_id, **node_attrs})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"directed": True,
|
||||||
|
"multigraph": False,
|
||||||
|
"graph": {},
|
||||||
|
"nodes": nodes_list,
|
||||||
|
"links": self.edges,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def extract_json_from_markdown(text: str) -> str:
|
||||||
|
"""从markdown代码块中提取JSON"""
|
||||||
|
text = text.strip()
|
||||||
|
if text.startswith("```json\n"):
|
||||||
|
text = text[8:]
|
||||||
|
if text.startswith("```\n"):
|
||||||
|
text = text[4:]
|
||||||
|
if text.endswith("\n```"):
|
||||||
|
text = text[:-4]
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def convert_to_type(val: str) -> Any:
|
||||||
|
"""将字符串值转换为适当的数据类型"""
|
||||||
|
if val == "True":
|
||||||
|
return True
|
||||||
|
if val == "False":
|
||||||
|
return False
|
||||||
|
if val == "?":
|
||||||
|
return None
|
||||||
|
if val.endswith(" g"):
|
||||||
|
return float(val.split(" ")[0])
|
||||||
|
if val.endswith("mg"):
|
||||||
|
return float(val.split("mg")[0])
|
||||||
|
elif val.endswith("mmol"):
|
||||||
|
return float(val.split("mmol")[0]) / 1000
|
||||||
|
elif val.endswith("mol"):
|
||||||
|
return float(val.split("mol")[0])
|
||||||
|
elif val.endswith("ml"):
|
||||||
|
return float(val.split("ml")[0])
|
||||||
|
elif val.endswith("RPM"):
|
||||||
|
return float(val.split("RPM")[0])
|
||||||
|
elif val.endswith(" °C"):
|
||||||
|
return float(val.split(" ")[0])
|
||||||
|
elif val.endswith(" %"):
|
||||||
|
return float(val.split(" ")[0])
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def refactor_data(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
"""统一的数据重构函数,根据操作类型自动选择模板"""
|
||||||
|
refactored_data = []
|
||||||
|
|
||||||
|
# 定义操作映射,包含生物实验和有机化学的所有操作
|
||||||
|
OPERATION_MAPPING = {
|
||||||
|
# 生物实验操作
|
||||||
|
"transfer_liquid": "SynBioFactory-liquid_handler.transfer_liquid",
|
||||||
|
"transfer": "SynBioFactory-liquid_handler.biomek-transfer",
|
||||||
|
"incubation": "SynBioFactory-liquid_handler.biomek-incubation",
|
||||||
|
"move_labware": "SynBioFactory-liquid_handler.biomek-move_labware",
|
||||||
|
"oscillation": "SynBioFactory-liquid_handler.biomek-oscillation",
|
||||||
|
# 有机化学操作
|
||||||
|
"HeatChillToTemp": "SynBioFactory-workstation-HeatChillProtocol",
|
||||||
|
"StopHeatChill": "SynBioFactory-workstation-HeatChillStopProtocol",
|
||||||
|
"StartHeatChill": "SynBioFactory-workstation-HeatChillStartProtocol",
|
||||||
|
"HeatChill": "SynBioFactory-workstation-HeatChillProtocol",
|
||||||
|
"Dissolve": "SynBioFactory-workstation-DissolveProtocol",
|
||||||
|
"Transfer": "SynBioFactory-workstation-TransferProtocol",
|
||||||
|
"Evaporate": "SynBioFactory-workstation-EvaporateProtocol",
|
||||||
|
"Recrystallize": "SynBioFactory-workstation-RecrystallizeProtocol",
|
||||||
|
"Filter": "SynBioFactory-workstation-FilterProtocol",
|
||||||
|
"Dry": "SynBioFactory-workstation-DryProtocol",
|
||||||
|
"Add": "SynBioFactory-workstation-AddProtocol",
|
||||||
|
}
|
||||||
|
|
||||||
|
UNSUPPORTED_OPERATIONS = ["Purge", "Wait", "Stir", "ResetHandling"]
|
||||||
|
|
||||||
|
for step in data:
|
||||||
|
operation = step.get("action")
|
||||||
|
if not operation or operation in UNSUPPORTED_OPERATIONS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 处理重复操作
|
||||||
|
if operation == "Repeat":
|
||||||
|
times = step.get("times", step.get("parameters", {}).get("times", 1))
|
||||||
|
sub_steps = step.get("steps", step.get("parameters", {}).get("steps", []))
|
||||||
|
for i in range(int(times)):
|
||||||
|
sub_data = refactor_data(sub_steps)
|
||||||
|
refactored_data.extend(sub_data)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 获取模板名称
|
||||||
|
template = OPERATION_MAPPING.get(operation)
|
||||||
|
if not template:
|
||||||
|
# 自动推断模板类型
|
||||||
|
if operation.lower() in ["transfer", "incubation", "move_labware", "oscillation"]:
|
||||||
|
template = f"SynBioFactory-liquid_handler.biomek-{operation}"
|
||||||
|
else:
|
||||||
|
template = f"SynBioFactory-workstation-{operation}Protocol"
|
||||||
|
|
||||||
|
# 创建步骤数据
|
||||||
|
step_data = {
|
||||||
|
"template": template,
|
||||||
|
"description": step.get("description", step.get("purpose", f"{operation} operation")),
|
||||||
|
"lab_node_type": "Device",
|
||||||
|
"parameters": step.get("parameters", step.get("action_args", {})),
|
||||||
|
}
|
||||||
|
refactored_data.append(step_data)
|
||||||
|
|
||||||
|
return refactored_data
|
||||||
|
|
||||||
|
|
||||||
|
def build_protocol_graph(
|
||||||
|
labware_info: List[Dict[str, Any]], protocol_steps: List[Dict[str, Any]], workstation_name: str
|
||||||
|
) -> SimpleGraph:
|
||||||
|
"""统一的协议图构建函数,根据设备类型自动选择构建逻辑"""
|
||||||
|
G = SimpleGraph()
|
||||||
|
resource_last_writer = {}
|
||||||
|
LAB_NAME = "SynBioFactory"
|
||||||
|
|
||||||
|
protocol_steps = refactor_data(protocol_steps)
|
||||||
|
|
||||||
|
# 检查协议步骤中的模板来判断协议类型
|
||||||
|
has_biomek_template = any("biomek" in step.get("template", "") for step in protocol_steps)
|
||||||
|
|
||||||
|
if has_biomek_template:
|
||||||
|
# 生物实验协议图构建
|
||||||
|
for labware_id, labware in labware_info.items():
|
||||||
|
node_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
labware_attrs = labware.copy()
|
||||||
|
labware_id = labware_attrs.pop("id", labware_attrs.get("name", f"labware_{uuid.uuid4()}"))
|
||||||
|
labware_attrs["description"] = labware_id
|
||||||
|
labware_attrs["lab_node_type"] = (
|
||||||
|
"Reagent" if "Plate" in str(labware_id) else "Labware" if "Rack" in str(labware_id) else "Sample"
|
||||||
|
)
|
||||||
|
labware_attrs["device_id"] = workstation_name
|
||||||
|
|
||||||
|
G.add_node(node_id, template=f"{LAB_NAME}-host_node-create_resource", **labware_attrs)
|
||||||
|
resource_last_writer[labware_id] = f"{node_id}:labware"
|
||||||
|
|
||||||
|
# 处理协议步骤
|
||||||
|
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")
|
||||||
|
prev_node = node_id
|
||||||
|
|
||||||
|
# 处理物料流
|
||||||
|
params = step.get("parameters", {})
|
||||||
|
if "sources" in params and params["sources"] in resource_last_writer:
|
||||||
|
source_node, source_port = resource_last_writer[params["sources"]].split(":")
|
||||||
|
G.add_edge(source_node, node_id, source_port=source_port, target_port="labware")
|
||||||
|
|
||||||
|
if "targets" in params:
|
||||||
|
resource_last_writer[params["targets"]] = f"{node_id}:labware"
|
||||||
|
|
||||||
|
# 添加协议结束节点
|
||||||
|
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")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 有机化学协议图构建
|
||||||
|
WORKSTATION_ID = workstation_name
|
||||||
|
|
||||||
|
# 为所有labware创建资源节点
|
||||||
|
for item_id, item in labware_info.items():
|
||||||
|
# item_id = item.get("id") or item.get("name", f"item_{uuid.uuid4()}")
|
||||||
|
node_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# 判断节点类型
|
||||||
|
if item.get("type") == "hardware" or "reactor" in str(item_id).lower():
|
||||||
|
if "reactor" not in str(item_id).lower():
|
||||||
|
continue
|
||||||
|
lab_node_type = "Sample"
|
||||||
|
description = f"Prepare Reactor: {item_id}"
|
||||||
|
liquid_type = []
|
||||||
|
liquid_volume = []
|
||||||
|
else:
|
||||||
|
lab_node_type = "Reagent"
|
||||||
|
description = f"Add Reagent to Flask: {item_id}"
|
||||||
|
liquid_type = [item_id]
|
||||||
|
liquid_volume = [1e5]
|
||||||
|
|
||||||
|
G.add_node(
|
||||||
|
node_id,
|
||||||
|
template=f"{LAB_NAME}-host_node-create_resource",
|
||||||
|
description=description,
|
||||||
|
lab_node_type=lab_node_type,
|
||||||
|
res_id=item_id,
|
||||||
|
device_id=WORKSTATION_ID,
|
||||||
|
class_name="container",
|
||||||
|
parent=WORKSTATION_ID,
|
||||||
|
bind_locations={"x": 0.0, "y": 0.0, "z": 0.0},
|
||||||
|
liquid_input_slot=[-1],
|
||||||
|
liquid_type=liquid_type,
|
||||||
|
liquid_volume=liquid_volume,
|
||||||
|
slot_on_deck="",
|
||||||
|
role=item.get("role", ""),
|
||||||
|
)
|
||||||
|
resource_last_writer[item_id] = f"{node_id}:labware"
|
||||||
|
|
||||||
|
last_control_node_id = None
|
||||||
|
|
||||||
|
# 处理协议步骤
|
||||||
|
for step in protocol_steps:
|
||||||
|
node_id = str(uuid.uuid4())
|
||||||
|
G.add_node(node_id, **step)
|
||||||
|
|
||||||
|
# 控制流
|
||||||
|
if last_control_node_id is not None:
|
||||||
|
G.add_edge(last_control_node_id, node_id, source_port="ready", target_port="ready")
|
||||||
|
last_control_node_id = node_id
|
||||||
|
|
||||||
|
# 物料流
|
||||||
|
params = step.get("parameters", {})
|
||||||
|
input_resources = {
|
||||||
|
"Vessel": params.get("vessel"),
|
||||||
|
"ToVessel": params.get("to_vessel"),
|
||||||
|
"FromVessel": params.get("from_vessel"),
|
||||||
|
"reagent": params.get("reagent"),
|
||||||
|
"solvent": params.get("solvent"),
|
||||||
|
"compound": params.get("compound"),
|
||||||
|
"sources": params.get("sources"),
|
||||||
|
"targets": params.get("targets"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for target_port, resource_name in input_resources.items():
|
||||||
|
if resource_name and resource_name in resource_last_writer:
|
||||||
|
source_node, source_port = resource_last_writer[resource_name].split(":")
|
||||||
|
G.add_edge(source_node, node_id, source_port=source_port, target_port=target_port)
|
||||||
|
|
||||||
|
output_resources = {
|
||||||
|
"VesselOut": params.get("vessel"),
|
||||||
|
"FromVesselOut": params.get("from_vessel"),
|
||||||
|
"ToVesselOut": params.get("to_vessel"),
|
||||||
|
"FiltrateOut": params.get("filtrate_vessel"),
|
||||||
|
"reagent": params.get("reagent"),
|
||||||
|
"solvent": params.get("solvent"),
|
||||||
|
"compound": params.get("compound"),
|
||||||
|
"sources_out": params.get("sources"),
|
||||||
|
"targets_out": params.get("targets"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for source_port, resource_name in output_resources.items():
|
||||||
|
if resource_name:
|
||||||
|
resource_last_writer[resource_name] = f"{node_id}:{source_port}"
|
||||||
|
|
||||||
|
return G
|
||||||
|
|
||||||
|
|
||||||
|
def draw_protocol_graph(protocol_graph: SimpleGraph, output_path: str):
|
||||||
|
"""
|
||||||
|
(辅助功能) 使用 networkx 和 matplotlib 绘制协议工作流图,用于可视化。
|
||||||
|
"""
|
||||||
|
if not protocol_graph:
|
||||||
|
print("Cannot draw graph: Graph object is empty.")
|
||||||
|
return
|
||||||
|
|
||||||
|
G = nx.DiGraph()
|
||||||
|
|
||||||
|
for node_id, attrs in protocol_graph.nodes.items():
|
||||||
|
label = attrs.get("description", attrs.get("template", node_id[:8]))
|
||||||
|
G.add_node(node_id, label=label, **attrs)
|
||||||
|
|
||||||
|
for edge in protocol_graph.edges:
|
||||||
|
G.add_edge(edge["source"], edge["target"])
|
||||||
|
|
||||||
|
plt.figure(figsize=(20, 15))
|
||||||
|
try:
|
||||||
|
pos = nx.nx_agraph.graphviz_layout(G, prog="dot")
|
||||||
|
except Exception:
|
||||||
|
pos = nx.shell_layout(G) # Fallback layout
|
||||||
|
|
||||||
|
node_labels = {node: data["label"] for node, data in G.nodes(data=True)}
|
||||||
|
nx.draw(
|
||||||
|
G,
|
||||||
|
pos,
|
||||||
|
with_labels=False,
|
||||||
|
node_size=2500,
|
||||||
|
node_color="skyblue",
|
||||||
|
node_shape="o",
|
||||||
|
edge_color="gray",
|
||||||
|
width=1.5,
|
||||||
|
arrowsize=15,
|
||||||
|
)
|
||||||
|
nx.draw_networkx_labels(G, pos, labels=node_labels, font_size=8, font_weight="bold")
|
||||||
|
|
||||||
|
plt.title("Chemical Protocol Workflow Graph", size=15)
|
||||||
|
plt.savefig(output_path, dpi=300, bbox_inches="tight")
|
||||||
|
plt.close()
|
||||||
|
print(f" - Visualization saved to '{output_path}'")
|
||||||
|
|
||||||
|
|
||||||
|
from networkx.drawing.nx_agraph import to_agraph
|
||||||
|
import re
|
||||||
|
|
||||||
|
COMPASS = {"n","e","s","w","ne","nw","se","sw","c"}
|
||||||
|
|
||||||
|
def _is_compass(port: str) -> bool:
|
||||||
|
return isinstance(port, str) and port.lower() in COMPASS
|
||||||
|
|
||||||
|
def draw_protocol_graph_with_ports(protocol_graph, output_path: str, rankdir: str = "LR"):
|
||||||
|
"""
|
||||||
|
使用 Graphviz 端口语法绘制协议工作流图。
|
||||||
|
- 若边上的 source_port/target_port 是 compass(n/e/s/w/...),直接用 compass。
|
||||||
|
- 否则自动为节点创建 record 形状并定义命名端口 <portname>。
|
||||||
|
最终由 PyGraphviz 渲染并输出到 output_path(后缀决定格式,如 .png/.svg/.pdf)。
|
||||||
|
"""
|
||||||
|
if not protocol_graph:
|
||||||
|
print("Cannot draw graph: Graph object is empty.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 1) 先用 networkx 搭建有向图,保留端口属性
|
||||||
|
G = nx.DiGraph()
|
||||||
|
for node_id, attrs in protocol_graph.nodes.items():
|
||||||
|
label = attrs.get("description", attrs.get("template", node_id[:8]))
|
||||||
|
# 保留一个干净的“中心标签”,用于放在 record 的中间槽
|
||||||
|
G.add_node(node_id, _core_label=str(label), **{k:v for k,v in attrs.items() if k not in ("label",)})
|
||||||
|
|
||||||
|
edges_data = []
|
||||||
|
in_ports_by_node = {} # 收集命名输入端口
|
||||||
|
out_ports_by_node = {} # 收集命名输出端口
|
||||||
|
|
||||||
|
for edge in protocol_graph.edges:
|
||||||
|
u = edge["source"]
|
||||||
|
v = edge["target"]
|
||||||
|
sp = edge.get("source_port")
|
||||||
|
tp = edge.get("target_port")
|
||||||
|
|
||||||
|
# 记录到图里(保留原始端口信息)
|
||||||
|
G.add_edge(u, v, source_port=sp, target_port=tp)
|
||||||
|
edges_data.append((u, v, sp, tp))
|
||||||
|
|
||||||
|
# 如果不是 compass,就按“命名端口”先归类,等会儿给节点造 record
|
||||||
|
if sp and not _is_compass(sp):
|
||||||
|
out_ports_by_node.setdefault(u, set()).add(str(sp))
|
||||||
|
if tp and not _is_compass(tp):
|
||||||
|
in_ports_by_node.setdefault(v, set()).add(str(tp))
|
||||||
|
|
||||||
|
# 2) 转为 AGraph,使用 Graphviz 渲染
|
||||||
|
A = to_agraph(G)
|
||||||
|
A.graph_attr.update(rankdir=rankdir, splines="true", concentrate="false", fontsize="10")
|
||||||
|
A.node_attr.update(shape="box", style="rounded,filled", fillcolor="lightyellow", color="#999999", fontname="Helvetica")
|
||||||
|
A.edge_attr.update(arrowsize="0.8", color="#666666")
|
||||||
|
|
||||||
|
# 3) 为需要命名端口的节点设置 record 形状与 label
|
||||||
|
# 左列 = 输入端口;中间 = 核心标签;右列 = 输出端口
|
||||||
|
for n in A.nodes():
|
||||||
|
node = A.get_node(n)
|
||||||
|
core = G.nodes[n].get("_core_label", n)
|
||||||
|
|
||||||
|
in_ports = sorted(in_ports_by_node.get(n, []))
|
||||||
|
out_ports = sorted(out_ports_by_node.get(n, []))
|
||||||
|
|
||||||
|
# 如果该节点涉及命名端口,则用 record;否则保留原 box
|
||||||
|
if in_ports or out_ports:
|
||||||
|
def port_fields(ports):
|
||||||
|
if not ports:
|
||||||
|
return " " # 必须留一个空槽占位
|
||||||
|
# 每个端口一个小格子,<p> name
|
||||||
|
return "|".join(f"<{re.sub(r'[^A-Za-z0-9_:.|-]', '_', p)}> {p}" for p in ports)
|
||||||
|
|
||||||
|
left = port_fields(in_ports)
|
||||||
|
right = port_fields(out_ports)
|
||||||
|
|
||||||
|
# 三栏:左(入) | 中(节点名) | 右(出)
|
||||||
|
record_label = f"{{ {left} | {core} | {right} }}"
|
||||||
|
node.attr.update(shape="record", label=record_label)
|
||||||
|
else:
|
||||||
|
# 没有命名端口:普通盒子,显示核心标签
|
||||||
|
node.attr.update(label=str(core))
|
||||||
|
|
||||||
|
# 4) 给边设置 headport / tailport
|
||||||
|
# - 若端口为 compass:直接用 compass(e.g., headport="e")
|
||||||
|
# - 若端口为命名端口:使用在 record 中定义的 <port> 名(同名即可)
|
||||||
|
for (u, v, sp, tp) in edges_data:
|
||||||
|
e = A.get_edge(u, v)
|
||||||
|
|
||||||
|
# Graphviz 属性:tail 是源,head 是目标
|
||||||
|
if sp:
|
||||||
|
if _is_compass(sp):
|
||||||
|
e.attr["tailport"] = sp.lower()
|
||||||
|
else:
|
||||||
|
# 与 record label 中 <port> 名一致;特殊字符已在 label 中做了清洗
|
||||||
|
e.attr["tailport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(sp))
|
||||||
|
|
||||||
|
if tp:
|
||||||
|
if _is_compass(tp):
|
||||||
|
e.attr["headport"] = tp.lower()
|
||||||
|
else:
|
||||||
|
e.attr["headport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(tp))
|
||||||
|
|
||||||
|
# 可选:若想让边更贴边缘,可设置 constraint/spline 等
|
||||||
|
# e.attr["arrowhead"] = "vee"
|
||||||
|
|
||||||
|
# 5) 输出
|
||||||
|
A.draw(output_path, prog="dot")
|
||||||
|
print(f" - Port-aware workflow rendered to '{output_path}'")
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_xdl_procedure(procedure_elem: ET.Element) -> List[ET.Element]:
|
||||||
|
"""展平嵌套的XDL程序结构"""
|
||||||
|
flattened_operations = []
|
||||||
|
TEMP_UNSUPPORTED_PROTOCOL = ["Purge", "Wait", "Stir", "ResetHandling"]
|
||||||
|
|
||||||
|
def extract_operations(element: ET.Element):
|
||||||
|
if element.tag not in ["Prep", "Reaction", "Workup", "Purification", "Procedure"]:
|
||||||
|
if element.tag not in TEMP_UNSUPPORTED_PROTOCOL:
|
||||||
|
flattened_operations.append(element)
|
||||||
|
|
||||||
|
for child in element:
|
||||||
|
extract_operations(child)
|
||||||
|
|
||||||
|
for child in procedure_elem:
|
||||||
|
extract_operations(child)
|
||||||
|
|
||||||
|
return flattened_operations
|
||||||
|
|
||||||
|
|
||||||
|
def parse_xdl_content(xdl_content: str) -> tuple:
|
||||||
|
"""解析XDL内容"""
|
||||||
|
try:
|
||||||
|
xdl_content_cleaned = "".join(c for c in xdl_content if c.isprintable())
|
||||||
|
root = ET.fromstring(xdl_content_cleaned)
|
||||||
|
|
||||||
|
synthesis_elem = root.find("Synthesis")
|
||||||
|
if synthesis_elem is None:
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
# 解析硬件组件
|
||||||
|
hardware_elem = synthesis_elem.find("Hardware")
|
||||||
|
hardware = []
|
||||||
|
if hardware_elem is not None:
|
||||||
|
hardware = [{"id": c.get("id"), "type": c.get("type")} for c in hardware_elem.findall("Component")]
|
||||||
|
|
||||||
|
# 解析试剂
|
||||||
|
reagents_elem = synthesis_elem.find("Reagents")
|
||||||
|
reagents = []
|
||||||
|
if reagents_elem is not None:
|
||||||
|
reagents = [{"name": r.get("name"), "role": r.get("role", "")} for r in reagents_elem.findall("Reagent")]
|
||||||
|
|
||||||
|
# 解析程序
|
||||||
|
procedure_elem = synthesis_elem.find("Procedure")
|
||||||
|
if procedure_elem is None:
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
flattened_operations = flatten_xdl_procedure(procedure_elem)
|
||||||
|
return hardware, reagents, flattened_operations
|
||||||
|
|
||||||
|
except ET.ParseError as e:
|
||||||
|
raise ValueError(f"Invalid XDL format: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def convert_xdl_to_dict(xdl_content: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
将XDL XML格式转换为标准的字典格式
|
||||||
|
|
||||||
|
Args:
|
||||||
|
xdl_content: XDL XML内容
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
转换结果,包含步骤和器材信息
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
hardware, reagents, flattened_operations = parse_xdl_content(xdl_content)
|
||||||
|
if hardware is None:
|
||||||
|
return {"error": "Failed to parse XDL content", "success": False}
|
||||||
|
|
||||||
|
# 将XDL元素转换为字典格式
|
||||||
|
steps_data = []
|
||||||
|
for elem in flattened_operations:
|
||||||
|
# 转换参数类型
|
||||||
|
parameters = {}
|
||||||
|
for key, val in elem.attrib.items():
|
||||||
|
converted_val = convert_to_type(val)
|
||||||
|
if converted_val is not None:
|
||||||
|
parameters[key] = converted_val
|
||||||
|
|
||||||
|
step_dict = {
|
||||||
|
"operation": elem.tag,
|
||||||
|
"parameters": parameters,
|
||||||
|
"description": elem.get("purpose", f"Operation: {elem.tag}"),
|
||||||
|
}
|
||||||
|
steps_data.append(step_dict)
|
||||||
|
|
||||||
|
# 合并硬件和试剂为统一的labware_info格式
|
||||||
|
labware_data = []
|
||||||
|
labware_data.extend({"id": hw["id"], "type": "hardware", **hw} for hw in hardware)
|
||||||
|
labware_data.extend({"name": reagent["name"], "type": "reagent", **reagent} for reagent in reagents)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"steps": steps_data,
|
||||||
|
"labware": labware_data,
|
||||||
|
"message": f"Successfully converted XDL to dict format. Found {len(steps_data)} steps and {len(labware_data)} labware items.",
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"XDL conversion failed: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return {"error": error_msg, "success": False}
|
||||||
|
|
||||||
|
|
||||||
|
def create_workflow(
|
||||||
|
steps_info: str,
|
||||||
|
labware_info: str,
|
||||||
|
workflow_name: str = "Generated Workflow",
|
||||||
|
workstation_name: str = "workstation",
|
||||||
|
workflow_description: str = "Auto-generated workflow from protocol",
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
创建工作流,输入数据已经是统一的字典格式
|
||||||
|
|
||||||
|
Args:
|
||||||
|
steps_info: 步骤信息 (JSON字符串,已经是list of dict格式)
|
||||||
|
labware_info: 实验器材和试剂信息 (JSON字符串,已经是list of dict格式)
|
||||||
|
workflow_name: 工作流名称
|
||||||
|
workflow_description: 工作流描述
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
创建结果,包含工作流UUID和详细信息
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 直接解析JSON数据
|
||||||
|
steps_info_clean = extract_json_from_markdown(steps_info)
|
||||||
|
labware_info_clean = extract_json_from_markdown(labware_info)
|
||||||
|
|
||||||
|
steps_data = json.loads(steps_info_clean)
|
||||||
|
labware_data = json.loads(labware_info_clean)
|
||||||
|
|
||||||
|
# 统一处理所有数据
|
||||||
|
protocol_graph = build_protocol_graph(labware_data, steps_data, workstation_name=workstation_name)
|
||||||
|
|
||||||
|
# 检测协议类型(用于标签)
|
||||||
|
protocol_type = "bio" if any("biomek" in step.get("template", "") for step in refactored_steps) else "organic"
|
||||||
|
|
||||||
|
# 转换为工作流格式
|
||||||
|
data = protocol_graph.to_dict()
|
||||||
|
|
||||||
|
# 转换节点格式
|
||||||
|
for i, node in enumerate(data["nodes"]):
|
||||||
|
description = node.get("description", "")
|
||||||
|
onode = {
|
||||||
|
"template": node.pop("template"),
|
||||||
|
"id": node["id"],
|
||||||
|
"lab_node_type": node.get("lab_node_type", "Device"),
|
||||||
|
"name": description or f"Node {i + 1}",
|
||||||
|
"params": {"default": node},
|
||||||
|
"handles": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 处理边连接
|
||||||
|
for edge in data["links"]:
|
||||||
|
if edge["source"] == node["id"]:
|
||||||
|
source_port = edge.get("source_port", "output")
|
||||||
|
if source_port not in onode["handles"]:
|
||||||
|
onode["handles"][source_port] = {"type": "source"}
|
||||||
|
|
||||||
|
if edge["target"] == node["id"]:
|
||||||
|
target_port = edge.get("target_port", "input")
|
||||||
|
if target_port not in onode["handles"]:
|
||||||
|
onode["handles"][target_port] = {"type": "target"}
|
||||||
|
|
||||||
|
data["nodes"][i] = onode
|
||||||
|
|
||||||
|
# 发送到API创建工作流
|
||||||
|
api_secret = configs.Lab.Key
|
||||||
|
if not api_secret:
|
||||||
|
return {"error": "API SecretKey is not configured", "success": False}
|
||||||
|
|
||||||
|
# Step 1: 创建工作流
|
||||||
|
workflow_url = f"{configs.Lab.Api}/api/v1/workflow/"
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
params = {"secret_key": api_secret}
|
||||||
|
|
||||||
|
graph_data = {"name": workflow_name, **data}
|
||||||
|
|
||||||
|
logger.info(f"Creating workflow: {workflow_name}")
|
||||||
|
response = requests.post(
|
||||||
|
workflow_url, params=params, json=graph_data, headers=headers, timeout=configs.Lab.Timeout
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
workflow_info = response.json()
|
||||||
|
|
||||||
|
if workflow_info.get("code") != 0:
|
||||||
|
error_msg = f"API returned an error: {workflow_info.get('msg', 'Unknown Error')}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return {"error": error_msg, "success": False}
|
||||||
|
|
||||||
|
workflow_uuid = workflow_info.get("data", {}).get("uuid")
|
||||||
|
if not workflow_uuid:
|
||||||
|
return {"error": "Failed to get workflow UUID from response", "success": False}
|
||||||
|
|
||||||
|
# Step 2: 添加到模板库(可选)
|
||||||
|
try:
|
||||||
|
library_url = f"{configs.Lab.Api}/api/flociety/vs/workflows/library/"
|
||||||
|
lib_payload = {
|
||||||
|
"workflow_uuid": workflow_uuid,
|
||||||
|
"title": workflow_name,
|
||||||
|
"description": workflow_description,
|
||||||
|
"labels": [protocol_type.title(), "Auto-generated"],
|
||||||
|
}
|
||||||
|
|
||||||
|
library_response = requests.post(
|
||||||
|
library_url, params=params, json=lib_payload, headers=headers, timeout=configs.Lab.Timeout
|
||||||
|
)
|
||||||
|
library_response.raise_for_status()
|
||||||
|
|
||||||
|
library_info = library_response.json()
|
||||||
|
logger.info(f"Workflow added to library: {library_info}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"workflow_uuid": workflow_uuid,
|
||||||
|
"workflow_info": workflow_info.get("data"),
|
||||||
|
"library_info": library_info.get("data"),
|
||||||
|
"protocol_type": protocol_type,
|
||||||
|
"message": f"Workflow '{workflow_name}' created successfully",
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 即使添加到库失败,工作流创建仍然成功
|
||||||
|
logger.warning(f"Failed to add workflow to library: {str(e)}")
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"workflow_uuid": workflow_uuid,
|
||||||
|
"workflow_info": workflow_info.get("data"),
|
||||||
|
"protocol_type": protocol_type,
|
||||||
|
"message": f"Workflow '{workflow_name}' created successfully (library addition failed)",
|
||||||
|
}
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
error_msg = f"Network error when calling API: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return {"error": error_msg, "success": False}
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
error_msg = f"JSON parsing error: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return {"error": error_msg, "success": False}
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"An unexpected error occurred: {str(e)}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return {"error": error_msg, "success": False}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
],
|
],
|
||||||
"parent": null,
|
"parent": null,
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "workstation.bioyond_dispensing_station",
|
"class": "bioyond_dispensing_station",
|
||||||
"config": {
|
"config": {
|
||||||
"config": {
|
"config": {
|
||||||
"api_key": "DE9BDDA0",
|
"api_key": "DE9BDDA0",
|
||||||
|
|||||||
186
test/workflow/example_bio.json
Normal file
186
test/workflow/example_bio.json
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
{
|
||||||
|
"workflow": [
|
||||||
|
{
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"action_args": {
|
||||||
|
"sources": "Liquid_1",
|
||||||
|
"targets": "Liquid_2",
|
||||||
|
"asp_vol": 66.0,
|
||||||
|
"dis_vol": 66.0,
|
||||||
|
"asp_flow_rate": 94.0,
|
||||||
|
"dis_flow_rate": 94.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"action_args": {
|
||||||
|
"sources": "Liquid_2",
|
||||||
|
"targets": "Liquid_3",
|
||||||
|
"asp_vol": 58.0,
|
||||||
|
"dis_vol": 96.0,
|
||||||
|
"asp_flow_rate": 94.0,
|
||||||
|
"dis_flow_rate": 94.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"action_args": {
|
||||||
|
"sources": "Liquid_4",
|
||||||
|
"targets": "Liquid_2",
|
||||||
|
"asp_vol": 85.0,
|
||||||
|
"dis_vol": 170.0,
|
||||||
|
"asp_flow_rate": 94.0,
|
||||||
|
"dis_flow_rate": 94.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"action_args": {
|
||||||
|
"sources": "Liquid_4",
|
||||||
|
"targets": "Liquid_2",
|
||||||
|
"asp_vol": 63.333333333333336,
|
||||||
|
"dis_vol": 170.0,
|
||||||
|
"asp_flow_rate": 94.0,
|
||||||
|
"dis_flow_rate": 94.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"action_args": {
|
||||||
|
"sources": "Liquid_2",
|
||||||
|
"targets": "Liquid_3",
|
||||||
|
"asp_vol": 72.0,
|
||||||
|
"dis_vol": 150.0,
|
||||||
|
"asp_flow_rate": 94.0,
|
||||||
|
"dis_flow_rate": 94.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"action_args": {
|
||||||
|
"sources": "Liquid_4",
|
||||||
|
"targets": "Liquid_2",
|
||||||
|
"asp_vol": 85.0,
|
||||||
|
"dis_vol": 170.0,
|
||||||
|
"asp_flow_rate": 94.0,
|
||||||
|
"dis_flow_rate": 94.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"action_args": {
|
||||||
|
"sources": "Liquid_4",
|
||||||
|
"targets": "Liquid_2",
|
||||||
|
"asp_vol": 63.333333333333336,
|
||||||
|
"dis_vol": 170.0,
|
||||||
|
"asp_flow_rate": 94.0,
|
||||||
|
"dis_flow_rate": 94.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"action_args": {
|
||||||
|
"sources": "Liquid_2",
|
||||||
|
"targets": "Liquid_3",
|
||||||
|
"asp_vol": 72.0,
|
||||||
|
"dis_vol": 150.0,
|
||||||
|
"asp_flow_rate": 94.0,
|
||||||
|
"dis_flow_rate": 94.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"action_args": {
|
||||||
|
"sources": "Liquid_2",
|
||||||
|
"targets": "Liquid_3",
|
||||||
|
"asp_vol": 20.0,
|
||||||
|
"dis_vol": 20.0,
|
||||||
|
"asp_flow_rate": 7.6,
|
||||||
|
"dis_flow_rate": 7.6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"action_args": {
|
||||||
|
"sources": "Liquid_5",
|
||||||
|
"targets": "Liquid_2",
|
||||||
|
"asp_vol": 6.0,
|
||||||
|
"dis_vol": 12.0,
|
||||||
|
"asp_flow_rate": 7.6,
|
||||||
|
"dis_flow_rate": 7.6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"action_args": {
|
||||||
|
"sources": "Liquid_5",
|
||||||
|
"targets": "Liquid_2",
|
||||||
|
"asp_vol": 10.666666666666666,
|
||||||
|
"dis_vol": 12.0,
|
||||||
|
"asp_flow_rate": 7.599999999999999,
|
||||||
|
"dis_flow_rate": 7.6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"action_args": {
|
||||||
|
"sources": "Liquid_2",
|
||||||
|
"targets": "Liquid_6",
|
||||||
|
"asp_vol": 12.0,
|
||||||
|
"dis_vol": 10.0,
|
||||||
|
"asp_flow_rate": 7.6,
|
||||||
|
"dis_flow_rate": 7.6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"reagent": {
|
||||||
|
"Liquid_6": {
|
||||||
|
"slot": 1,
|
||||||
|
"well": [
|
||||||
|
"A2"
|
||||||
|
],
|
||||||
|
"labware": "elution plate"
|
||||||
|
},
|
||||||
|
"Liquid_1": {
|
||||||
|
"slot": 2,
|
||||||
|
"well": [
|
||||||
|
"A1",
|
||||||
|
"A2",
|
||||||
|
"A4"
|
||||||
|
],
|
||||||
|
"labware": "reagent reservoir"
|
||||||
|
},
|
||||||
|
"Liquid_4": {
|
||||||
|
"slot": 2,
|
||||||
|
"well": [
|
||||||
|
"A1",
|
||||||
|
"A2",
|
||||||
|
"A4"
|
||||||
|
],
|
||||||
|
"labware": "reagent reservoir"
|
||||||
|
},
|
||||||
|
"Liquid_5": {
|
||||||
|
"slot": 2,
|
||||||
|
"well": [
|
||||||
|
"A1",
|
||||||
|
"A2",
|
||||||
|
"A4"
|
||||||
|
],
|
||||||
|
"labware": "reagent reservoir"
|
||||||
|
},
|
||||||
|
"Liquid_2": {
|
||||||
|
"slot": 4,
|
||||||
|
"well": [
|
||||||
|
"A2"
|
||||||
|
],
|
||||||
|
"labware": "TAG1 plate on Magnetic Module GEN2"
|
||||||
|
},
|
||||||
|
"Liquid_3": {
|
||||||
|
"slot": 12,
|
||||||
|
"well": [
|
||||||
|
"A1"
|
||||||
|
],
|
||||||
|
"labware": "Opentrons Fixed Trash"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
test/workflow/merge_workflow.py
Normal file
14
test/workflow/merge_workflow.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import pytest
|
||||||
|
import json
|
||||||
|
from scripts.workflow import build_protocol_graph, draw_protocol_graph, draw_protocol_graph_with_ports
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("protocol_name", [
|
||||||
|
"example_bio",
|
||||||
|
# "bioyond_materials_liquidhandling_1",
|
||||||
|
])
|
||||||
|
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")
|
||||||
|
print(graph)
|
||||||
@@ -126,12 +126,16 @@ class HTTPClient:
|
|||||||
Returns:
|
Returns:
|
||||||
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
|
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
|
||||||
"""
|
"""
|
||||||
|
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_get.json"), "w", encoding="utf-8") as f:
|
||||||
|
f.write(json.dumps({"uuids": uuid_list, "with_children": with_children}, indent=4))
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.remote_addr}/edge/material/query",
|
f"{self.remote_addr}/edge/material/query",
|
||||||
json={"uuids": uuid_list, "with_children": with_children},
|
json={"uuids": uuid_list, "with_children": with_children},
|
||||||
headers={"Authorization": f"Lab {self.auth}"},
|
headers={"Authorization": f"Lab {self.auth}"},
|
||||||
timeout=100,
|
timeout=100,
|
||||||
)
|
)
|
||||||
|
with open(os.path.join(BasicConfig.working_dir, "res_resource_tree_get.json"), "w", encoding="utf-8") as f:
|
||||||
|
f.write(f"{response.status_code}" + "\n" + response.text)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
res = response.json()
|
res = response.json()
|
||||||
if "code" in res and res["code"] != 0:
|
if "code" in res and res["code"] != 0:
|
||||||
@@ -187,12 +191,16 @@ class HTTPClient:
|
|||||||
Returns:
|
Returns:
|
||||||
Dict: 返回的资源数据
|
Dict: 返回的资源数据
|
||||||
"""
|
"""
|
||||||
|
with open(os.path.join(BasicConfig.working_dir, "req_resource_get.json"), "w", encoding="utf-8") as f:
|
||||||
|
f.write(json.dumps({"id": id, "with_children": with_children}, indent=4))
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
f"{self.remote_addr}/lab/material",
|
f"{self.remote_addr}/lab/material",
|
||||||
params={"id": id, "with_children": with_children},
|
params={"id": id, "with_children": with_children},
|
||||||
headers={"Authorization": f"Lab {self.auth}"},
|
headers={"Authorization": f"Lab {self.auth}"},
|
||||||
timeout=20,
|
timeout=20,
|
||||||
)
|
)
|
||||||
|
with open(os.path.join(BasicConfig.working_dir, "res_resource_get.json"), "w", encoding="utf-8") as f:
|
||||||
|
f.write(f"{response.status_code}" + "\n" + response.text)
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
def resource_del(self, id: str) -> requests.Response:
|
def resource_del(self, id: str) -> requests.Response:
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
workflow_id = "3a15d4a1-3bbe-76f9-a458-292896a338f5"
|
workflow_id = "3a15d4a1-3bbe-76f9-a458-292896a338f5"
|
||||||
|
|
||||||
# 4. 查询工作流对应的holdMID
|
# 4. 查询工作流对应的holdMID
|
||||||
material_info = self.material_id_query(workflow_id)
|
material_info = self.hardware_interface.material_id_query(workflow_id)
|
||||||
if not material_info:
|
if not material_info:
|
||||||
raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息")
|
raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息")
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
|
|||||||
|
|
||||||
class BioyondReactionStation(BioyondWorkstation):
|
class BioyondReactionStation(BioyondWorkstation):
|
||||||
"""Bioyond反应站类
|
"""Bioyond反应站类
|
||||||
|
|
||||||
继承自BioyondWorkstation,提供反应站特定的业务方法
|
继承自BioyondWorkstation,提供反应站特定的业务方法
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs):
|
def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs):
|
||||||
"""初始化反应站
|
"""初始化反应站
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config: 配置字典,应包含workflow_mappings等配置
|
config: 配置字典,应包含workflow_mappings等配置
|
||||||
deck: Deck对象
|
deck: Deck对象
|
||||||
@@ -27,13 +27,13 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
"""
|
"""
|
||||||
if deck is None and config:
|
if deck is None and config:
|
||||||
deck = config.get('deck')
|
deck = config.get('deck')
|
||||||
|
|
||||||
print(f"BioyondReactionStation初始化 - config包含workflow_mappings: {'workflow_mappings' in (config or {})}")
|
print(f"BioyondReactionStation初始化 - config包含workflow_mappings: {'workflow_mappings' in (config or {})}")
|
||||||
if config and 'workflow_mappings' in config:
|
if config and 'workflow_mappings' in config:
|
||||||
print(f"workflow_mappings内容: {config['workflow_mappings']}")
|
print(f"workflow_mappings内容: {config['workflow_mappings']}")
|
||||||
|
|
||||||
super().__init__(bioyond_config=config, deck=deck)
|
super().__init__(bioyond_config=config, deck=deck)
|
||||||
|
|
||||||
print(f"BioyondReactionStation初始化完成 - workflow_mappings: {self.workflow_mappings}")
|
print(f"BioyondReactionStation初始化完成 - workflow_mappings: {self.workflow_mappings}")
|
||||||
print(f"workflow_mappings长度: {len(self.workflow_mappings)}")
|
print(f"workflow_mappings长度: {len(self.workflow_mappings)}")
|
||||||
|
|
||||||
@@ -49,21 +49,21 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
def reactor_taken_in(
|
def reactor_taken_in(
|
||||||
self,
|
self,
|
||||||
assign_material_name: str,
|
assign_material_name: str,
|
||||||
cutoff: str = "900000",
|
cutoff: str = "900000",
|
||||||
temperature: float = -10.00
|
temperature: float = -10.00
|
||||||
):
|
):
|
||||||
"""反应器放入
|
"""反应器放入
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
assign_material_name: 物料名称(不能为空)
|
assign_material_name: 物料名称(不能为空)
|
||||||
cutoff: 截止值/通量配置(需为有效数字字符串,默认 "900000")
|
cutoff: 粘度上限(需为有效数字字符串,默认 "900000")
|
||||||
temperature: 温度上限(°C,范围:-50.00 至 100.00)
|
temperature: 温度设定(°C,范围:-50.00 至 100.00)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: JSON 字符串,格式为 {"suc": True}
|
str: JSON 字符串,格式为 {"suc": True}
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: 若物料名称无效或 cutoff 格式错误
|
ValueError: 若物料名称无效或 cutoff 格式错误
|
||||||
"""
|
"""
|
||||||
@@ -73,7 +73,7 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
float(cutoff)
|
float(cutoff)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ValueError("cutoff 必须是有效的数字字符串")
|
raise ValueError("cutoff 必须是有效的数字字符串")
|
||||||
|
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_in"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_in"}')
|
||||||
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||||
if material_id is None:
|
if material_id is None:
|
||||||
@@ -103,21 +103,21 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
def solid_feeding_vials(
|
def solid_feeding_vials(
|
||||||
self,
|
self,
|
||||||
material_id: str,
|
material_id: str,
|
||||||
time: str = "0",
|
time: str = "0",
|
||||||
torque_variation: int = 1,
|
torque_variation: int = 1,
|
||||||
assign_material_name: str = None,
|
assign_material_name: str = None,
|
||||||
temperature: float = 25.00
|
temperature: float = 25.00
|
||||||
):
|
):
|
||||||
"""固体进料小瓶
|
"""固体进料小瓶
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
material_id: 粉末类型ID
|
material_id: 粉末类型ID,1=盐(21分钟),2=面粉(27分钟),3=BTDA(38分钟)
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
assign_material_name: 物料名称(用于获取试剂瓶位ID)
|
assign_material_name: 物料名称(用于获取试剂瓶位ID)
|
||||||
temperature: 温度上限(°C)
|
temperature: 温度设定(°C)
|
||||||
"""
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}')
|
||||||
material_id_m = self.hardware_interface._get_material_id_by_name(assign_material_name) if assign_material_name else None
|
material_id_m = self.hardware_interface._get_material_id_by_name(assign_material_name) if assign_material_name else None
|
||||||
@@ -127,7 +127,7 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
|
|
||||||
feeding_step_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["feeding"]
|
feeding_step_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["feeding"]
|
||||||
observe_step_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["observe"]
|
observe_step_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["observe"]
|
||||||
|
|
||||||
solid_feeding_vials_params = {
|
solid_feeding_vials_params = {
|
||||||
"param_values": {
|
"param_values": {
|
||||||
feeding_step_id: {
|
feeding_step_id: {
|
||||||
@@ -152,22 +152,22 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
def liquid_feeding_vials_non_titration(
|
def liquid_feeding_vials_non_titration(
|
||||||
self,
|
self,
|
||||||
volume_formula: str,
|
volume_formula: str,
|
||||||
assign_material_name: str,
|
assign_material_name: str,
|
||||||
titration_type: str = "1",
|
titration_type: str = "1",
|
||||||
time: str = "0",
|
time: str = "0",
|
||||||
torque_variation: int = 1,
|
torque_variation: int = 1,
|
||||||
temperature: float = 25.00
|
temperature: float = 25.00
|
||||||
):
|
):
|
||||||
"""液体进料小瓶(非滴定)
|
"""液体进料小瓶(非滴定)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
volume_formula: 分液公式(μL)
|
volume_formula: 分液公式(μL)
|
||||||
assign_material_name: 物料名称
|
assign_material_name: 物料名称
|
||||||
titration_type: 是否滴定(1=滴定, 其他=非滴定)
|
titration_type: 是否滴定(1=否, 2=是)
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
temperature: 温度(°C)
|
temperature: 温度(°C)
|
||||||
"""
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}')
|
||||||
@@ -180,7 +180,7 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
|
|
||||||
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["liquid"]
|
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["liquid"]
|
||||||
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["observe"]
|
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["observe"]
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"param_values": {
|
"param_values": {
|
||||||
liquid_step_id: {
|
liquid_step_id: {
|
||||||
@@ -206,23 +206,23 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
def liquid_feeding_solvents(
|
def liquid_feeding_solvents(
|
||||||
self,
|
self,
|
||||||
assign_material_name: str,
|
assign_material_name: str,
|
||||||
volume: str,
|
volume: str,
|
||||||
titration_type: str = "1",
|
titration_type: str = "1",
|
||||||
time: str = "360",
|
time: str = "360",
|
||||||
torque_variation: int = 2,
|
torque_variation: int = 2,
|
||||||
temperature: float = 25.00
|
temperature: float = 25.00
|
||||||
):
|
):
|
||||||
"""液体进料-溶剂
|
"""液体进料-溶剂
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
assign_material_name: 物料名称
|
assign_material_name: 物料名称
|
||||||
volume: 分液量(μL)
|
volume: 分液量(μL)
|
||||||
titration_type: 是否滴定
|
titration_type: 是否滴定(1=否, 2=是)
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
temperature: 温度上限(°C)
|
temperature: 温度设定(°C)
|
||||||
"""
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}')
|
||||||
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||||
@@ -231,10 +231,10 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
|
|
||||||
if isinstance(temperature, str):
|
if isinstance(temperature, str):
|
||||||
temperature = float(temperature)
|
temperature = float(temperature)
|
||||||
|
|
||||||
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["liquid"]
|
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["liquid"]
|
||||||
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["observe"]
|
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["observe"]
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"param_values": {
|
"param_values": {
|
||||||
liquid_step_id: {
|
liquid_step_id: {
|
||||||
@@ -260,22 +260,22 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
def liquid_feeding_titration(
|
def liquid_feeding_titration(
|
||||||
self,
|
self,
|
||||||
volume_formula: str,
|
volume_formula: str,
|
||||||
assign_material_name: str,
|
assign_material_name: str,
|
||||||
titration_type: str = "1",
|
titration_type: str = "1",
|
||||||
time: str = "90",
|
time: str = "90",
|
||||||
torque_variation: int = 2,
|
torque_variation: int = 2,
|
||||||
temperature: float = 25.00
|
temperature: float = 25.00
|
||||||
):
|
):
|
||||||
"""液体进料(滴定)
|
"""液体进料(滴定)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
volume_formula: 分液公式(μL)
|
volume_formula: 分液公式(μL)
|
||||||
assign_material_name: 物料名称
|
assign_material_name: 物料名称
|
||||||
titration_type: 是否滴定
|
titration_type: 是否滴定(1=否, 2=是)
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
temperature: 温度(°C)
|
temperature: 温度(°C)
|
||||||
"""
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
|
||||||
@@ -314,23 +314,23 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
def liquid_feeding_beaker(
|
def liquid_feeding_beaker(
|
||||||
self,
|
self,
|
||||||
volume: str = "35000",
|
volume: str = "35000",
|
||||||
assign_material_name: str = "BAPP",
|
assign_material_name: str = "BAPP",
|
||||||
time: str = "0",
|
time: str = "0",
|
||||||
torque_variation: int = 1,
|
torque_variation: int = 1,
|
||||||
titration_type: str = "1",
|
titration_type: str = "1",
|
||||||
temperature: float = 25.00
|
temperature: float = 25.00
|
||||||
):
|
):
|
||||||
"""液体进料烧杯
|
"""液体进料烧杯
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
volume: 分液量(μL)
|
volume: 分液量(μL)
|
||||||
assign_material_name: 物料名称(试剂瓶位)
|
assign_material_name: 物料名称(试剂瓶位)
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
titration_type: 是否滴定
|
titration_type: 是否滴定(1=否, 2=是)
|
||||||
temperature: 温度上限(°C)
|
temperature: 温度设定(°C)
|
||||||
"""
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}')
|
||||||
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||||
@@ -366,7 +366,7 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"成功添加液体进料烧杯参数: volume={volume}μL, material={assign_material_name}->ID:{material_id}")
|
print(f"成功添加液体进料烧杯参数: volume={volume}μL, material={assign_material_name}->ID:{material_id}")
|
||||||
print(f"当前队列长度: {len(self.pending_task_params)}")
|
print(f"当前队列长度: {len(self.pending_task_params)}")
|
||||||
return json.dumps({"suc": True})
|
return json.dumps({"suc": True})
|
||||||
|
|
||||||
def drip_back(
|
def drip_back(
|
||||||
self,
|
self,
|
||||||
assign_material_name: str,
|
assign_material_name: str,
|
||||||
@@ -377,13 +377,13 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
temperature: float = 25.00
|
temperature: float = 25.00
|
||||||
):
|
):
|
||||||
"""滴回去
|
"""滴回去
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
assign_material_name: 物料名称(液体种类)
|
assign_material_name: 物料名称(液体种类)
|
||||||
volume: 分液量(μL)
|
volume: 分液量(μL)
|
||||||
titration_type: 是否滴定
|
titration_type: 是否滴定(1=否, 2=是)
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
temperature: 温度(°C)
|
temperature: 温度(°C)
|
||||||
"""
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "drip_back"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "drip_back"}')
|
||||||
@@ -425,7 +425,7 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
|
|
||||||
def get_workflow_sequence(self) -> List[str]:
|
def get_workflow_sequence(self) -> List[str]:
|
||||||
"""获取当前工作流执行顺序
|
"""获取当前工作流执行顺序
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
工作流名称列表
|
工作流名称列表
|
||||||
"""
|
"""
|
||||||
@@ -439,10 +439,10 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
|
|
||||||
def workflow_step_query(self, workflow_id: str) -> dict:
|
def workflow_step_query(self, workflow_id: str) -> dict:
|
||||||
"""查询工作流步骤参数
|
"""查询工作流步骤参数
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
workflow_id: 工作流ID
|
workflow_id: 工作流ID
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
工作流步骤参数字典
|
工作流步骤参数字典
|
||||||
"""
|
"""
|
||||||
@@ -450,10 +450,10 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
|
|
||||||
def create_order(self, json_str: str) -> dict:
|
def create_order(self, json_str: str) -> dict:
|
||||||
"""创建订单
|
"""创建订单
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
json_str: 订单参数的JSON字符串
|
json_str: 订单参数的JSON字符串
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
创建结果
|
创建结果
|
||||||
"""
|
"""
|
||||||
@@ -463,10 +463,10 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
|
|
||||||
def process_web_workflows(self, web_workflow_json: str) -> List[Dict[str, str]]:
|
def process_web_workflows(self, web_workflow_json: str) -> List[Dict[str, str]]:
|
||||||
"""处理网页工作流列表
|
"""处理网页工作流列表
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
web_workflow_json: JSON 格式的网页工作流列表
|
web_workflow_json: JSON 格式的网页工作流列表
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[Dict[str, str]]: 包含工作流 ID 和名称的字典列表
|
List[Dict[str, str]]: 包含工作流 ID 和名称的字典列表
|
||||||
"""
|
"""
|
||||||
@@ -492,11 +492,11 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict:
|
def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict:
|
||||||
"""
|
"""
|
||||||
一站式处理工作流程:解析网页工作流列表,合并工作流(带参数),然后发布任务
|
一站式处理工作流程:解析网页工作流列表,合并工作流(带参数),然后发布任务
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
workflow_name: 合并后的工作流名称
|
workflow_name: 合并后的工作流名称
|
||||||
task_name: 任务名称
|
task_name: 任务名称
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
任务创建结果
|
任务创建结果
|
||||||
"""
|
"""
|
||||||
@@ -504,32 +504,32 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"\n{'='*60}")
|
print(f"\n{'='*60}")
|
||||||
print(f"📋 处理网页工作流列表: {web_workflow_list}")
|
print(f"📋 处理网页工作流列表: {web_workflow_list}")
|
||||||
print(f"{'='*60}")
|
print(f"{'='*60}")
|
||||||
|
|
||||||
web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list})
|
web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list})
|
||||||
workflows_result = self.process_web_workflows(web_workflow_json)
|
workflows_result = self.process_web_workflows(web_workflow_json)
|
||||||
|
|
||||||
if not workflows_result:
|
if not workflows_result:
|
||||||
return self._create_error_result("处理网页工作流列表失败", "process_web_workflows")
|
return self._create_error_result("处理网页工作流列表失败", "process_web_workflows")
|
||||||
|
|
||||||
print(f"workflows_result 类型: {type(workflows_result)}")
|
print(f"workflows_result 类型: {type(workflows_result)}")
|
||||||
print(f"workflows_result 内容: {workflows_result}")
|
print(f"workflows_result 内容: {workflows_result}")
|
||||||
|
|
||||||
workflows_with_params = self._build_workflows_with_parameters(workflows_result)
|
workflows_with_params = self._build_workflows_with_parameters(workflows_result)
|
||||||
|
|
||||||
merge_data = {
|
merge_data = {
|
||||||
"name": workflow_name,
|
"name": workflow_name,
|
||||||
"workflows": workflows_with_params
|
"workflows": workflows_with_params
|
||||||
}
|
}
|
||||||
|
|
||||||
# print(f"\n🔄 合并工作流(带参数),名称: {workflow_name}")
|
# print(f"\n🔄 合并工作流(带参数),名称: {workflow_name}")
|
||||||
merged_workflow = self.merge_workflow_with_parameters(json.dumps(merge_data))
|
merged_workflow = self.merge_workflow_with_parameters(json.dumps(merge_data))
|
||||||
|
|
||||||
if not merged_workflow:
|
if not merged_workflow:
|
||||||
return self._create_error_result("合并工作流失败", "merge_workflow_with_parameters")
|
return self._create_error_result("合并工作流失败", "merge_workflow_with_parameters")
|
||||||
|
|
||||||
workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "")
|
workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "")
|
||||||
# print(f"\n📤 使用工作流创建任务: {workflow_name} (ID: {workflow_id})")
|
# print(f"\n📤 使用工作流创建任务: {workflow_name} (ID: {workflow_id})")
|
||||||
|
|
||||||
order_params = [{
|
order_params = [{
|
||||||
"orderCode": f"task_{self.hardware_interface.get_current_time_iso8601()}",
|
"orderCode": f"task_{self.hardware_interface.get_current_time_iso8601()}",
|
||||||
"orderName": task_name,
|
"orderName": task_name,
|
||||||
@@ -537,16 +537,16 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
"borderNumber": 1,
|
"borderNumber": 1,
|
||||||
"paramValues": {}
|
"paramValues": {}
|
||||||
}]
|
}]
|
||||||
|
|
||||||
result = self.create_order(json.dumps(order_params))
|
result = self.create_order(json.dumps(order_params))
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
return self._create_error_result("创建任务失败", "create_order")
|
return self._create_error_result("创建任务失败", "create_order")
|
||||||
|
|
||||||
# 清空工作流序列和参数,防止下次执行时累积重复
|
# 清空工作流序列和参数,防止下次执行时累积重复
|
||||||
self.pending_task_params = []
|
self.pending_task_params = []
|
||||||
self.clear_workflows()
|
self.clear_workflows() # 清空工作流序列,避免重复累积
|
||||||
|
|
||||||
# print(f"\n✅ 任务创建成功: {result}")
|
# print(f"\n✅ 任务创建成功: {result}")
|
||||||
# print(f"\n✅ 任务创建成功")
|
# print(f"\n✅ 任务创建成功")
|
||||||
print(f"{'='*60}\n")
|
print(f"{'='*60}\n")
|
||||||
@@ -555,10 +555,10 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
def _build_workflows_with_parameters(self, workflows_result: list) -> list:
|
def _build_workflows_with_parameters(self, workflows_result: list) -> list:
|
||||||
"""
|
"""
|
||||||
构建带参数的工作流列表
|
构建带参数的工作流列表
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
workflows_result: 处理后的工作流列表(应为包含 id 和 name 的字典列表)
|
workflows_result: 处理后的工作流列表(应为包含 id 和 name 的字典列表)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
符合新接口格式的工作流参数结构
|
符合新接口格式的工作流参数结构
|
||||||
"""
|
"""
|
||||||
@@ -577,19 +577,19 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
continue
|
continue
|
||||||
workflow_name = workflow_info.get("name", "")
|
workflow_name = workflow_info.get("name", "")
|
||||||
# print(f"\n🔧 处理工作流 [{idx}]: {workflow_name} (ID: {workflow_id})")
|
# print(f"\n🔧 处理工作流 [{idx}]: {workflow_name} (ID: {workflow_id})")
|
||||||
|
|
||||||
if idx >= len(self.pending_task_params):
|
if idx >= len(self.pending_task_params):
|
||||||
# print(f" ⚠️ 无对应参数,跳过")
|
# print(f" ⚠️ 无对应参数,跳过")
|
||||||
workflows_with_params.append({"id": workflow_id})
|
workflows_with_params.append({"id": workflow_id})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
param_data = self.pending_task_params[idx]
|
param_data = self.pending_task_params[idx]
|
||||||
param_values = param_data.get("param_values", {})
|
param_values = param_data.get("param_values", {})
|
||||||
if not param_values:
|
if not param_values:
|
||||||
# print(f" ⚠️ 参数为空,跳过")
|
# print(f" ⚠️ 参数为空,跳过")
|
||||||
workflows_with_params.append({"id": workflow_id})
|
workflows_with_params.append({"id": workflow_id})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
step_parameters = {}
|
step_parameters = {}
|
||||||
for step_id, actions_dict in param_values.items():
|
for step_id, actions_dict in param_values.items():
|
||||||
# print(f" 📍 步骤ID: {step_id}")
|
# print(f" 📍 步骤ID: {step_id}")
|
||||||
@@ -646,25 +646,25 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
def merge_workflow_with_parameters(self, json_str: str) -> dict:
|
def merge_workflow_with_parameters(self, json_str: str) -> dict:
|
||||||
"""
|
"""
|
||||||
调用新接口:合并工作流并传递参数
|
调用新接口:合并工作流并传递参数
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
json_str: JSON格式的字符串,包含:
|
json_str: JSON格式的字符串,包含:
|
||||||
- name: 工作流名称
|
- name: 工作流名称
|
||||||
- workflows: [{"id": "工作流ID", "stepParameters": {...}}]
|
- workflows: [{"id": "工作流ID", "stepParameters": {...}}]
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
合并后的工作流信息
|
合并后的工作流信息
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = json.loads(json_str)
|
data = json.loads(json_str)
|
||||||
|
|
||||||
# 在工作流名称后面添加时间戳,避免重复
|
# 在工作流名称后面添加时间戳,避免重复
|
||||||
if "name" in data and data["name"]:
|
if "name" in data and data["name"]:
|
||||||
timestamp = self.hardware_interface.get_current_time_iso8601().replace(":", "-").replace(".", "-")
|
timestamp = self.hardware_interface.get_current_time_iso8601().replace(":", "-").replace(".", "-")
|
||||||
original_name = data["name"]
|
original_name = data["name"]
|
||||||
data["name"] = f"{original_name}_{timestamp}"
|
data["name"] = f"{original_name}_{timestamp}"
|
||||||
print(f"🕒 工作流名称已添加时间戳: {original_name} -> {data['name']}")
|
print(f"🕒 工作流名称已添加时间戳: {original_name} -> {data['name']}")
|
||||||
|
|
||||||
request_data = {
|
request_data = {
|
||||||
"apiKey": API_CONFIG["api_key"],
|
"apiKey": API_CONFIG["api_key"],
|
||||||
"requestTime": self.hardware_interface.get_current_time_iso8601(),
|
"requestTime": self.hardware_interface.get_current_time_iso8601(),
|
||||||
@@ -673,37 +673,37 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
print(f"\n📤 发送合并请求:")
|
print(f"\n📤 发送合并请求:")
|
||||||
print(f" 工作流名称: {data.get('name')}")
|
print(f" 工作流名称: {data.get('name')}")
|
||||||
print(f" 子工作流数量: {len(data.get('workflows', []))}")
|
print(f" 子工作流数量: {len(data.get('workflows', []))}")
|
||||||
|
|
||||||
# 打印完整的POST请求内容
|
# 打印完整的POST请求内容
|
||||||
print(f"\n🔍 POST请求详细内容:")
|
print(f"\n🔍 POST请求详细内容:")
|
||||||
print(f" URL: {self.hardware_interface.host}/api/lims/workflow/merge-workflow-with-parameters")
|
print(f" URL: {self.hardware_interface.host}/api/lims/workflow/merge-workflow-with-parameters")
|
||||||
print(f" Headers: {{'Content-Type': 'application/json'}}")
|
print(f" Headers: {{'Content-Type': 'application/json'}}")
|
||||||
print(f" Request Data:")
|
print(f" Request Data:")
|
||||||
print(f" {json.dumps(request_data, indent=4, ensure_ascii=False)}")
|
print(f" {json.dumps(request_data, indent=4, ensure_ascii=False)}")
|
||||||
#
|
#
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.hardware_interface.host}/api/lims/workflow/merge-workflow-with-parameters",
|
f"{self.hardware_interface.host}/api/lims/workflow/merge-workflow-with-parameters",
|
||||||
json=request_data,
|
json=request_data,
|
||||||
headers={"Content-Type": "application/json"},
|
headers={"Content-Type": "application/json"},
|
||||||
timeout=30
|
timeout=30
|
||||||
)
|
)
|
||||||
|
|
||||||
# # 打印响应详细内容
|
# # 打印响应详细内容
|
||||||
# print(f"\n📥 POST响应详细内容:")
|
# print(f"\n📥 POST响应详细内容:")
|
||||||
# print(f" 状态码: {response.status_code}")
|
# print(f" 状态码: {response.status_code}")
|
||||||
# print(f" 响应头: {dict(response.headers)}")
|
# print(f" 响应头: {dict(response.headers)}")
|
||||||
# print(f" 响应体: {response.text}")
|
# print(f" 响应体: {response.text}")
|
||||||
# #
|
# #
|
||||||
try:
|
try:
|
||||||
result = response.json()
|
result = response.json()
|
||||||
# #
|
# #
|
||||||
# print(f"\n📋 解析后的响应JSON:")
|
# print(f"\n📋 解析后的响应JSON:")
|
||||||
# print(f" {json.dumps(result, indent=4, ensure_ascii=False)}")
|
# print(f" {json.dumps(result, indent=4, ensure_ascii=False)}")
|
||||||
# #
|
# #
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
print(f"❌ 服务器返回非 JSON 格式响应: {response.text}")
|
print(f"❌ 服务器返回非 JSON 格式响应: {response.text}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if result.get("code") == 1:
|
if result.get("code") == 1:
|
||||||
print(f"✅ 工作流合并成功(带参数)")
|
print(f"✅ 工作流合并成功(带参数)")
|
||||||
return result.get("data", {})
|
return result.get("data", {})
|
||||||
@@ -711,7 +711,7 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
error_msg = result.get('message', '未知错误')
|
error_msg = result.get('message', '未知错误')
|
||||||
print(f"❌ 工作流合并失败: {error_msg}")
|
print(f"❌ 工作流合并失败: {error_msg}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except requests.exceptions.Timeout:
|
except requests.exceptions.Timeout:
|
||||||
print(f"❌ 合并工作流请求超时")
|
print(f"❌ 合并工作流请求超时")
|
||||||
return None
|
return None
|
||||||
@@ -727,10 +727,10 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
|
|
||||||
def _validate_and_refresh_workflow_if_needed(self, workflow_name: str) -> bool:
|
def _validate_and_refresh_workflow_if_needed(self, workflow_name: str) -> bool:
|
||||||
"""验证工作流ID是否有效,如果无效则重新合并
|
"""验证工作流ID是否有效,如果无效则重新合并
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
workflow_name: 工作流名称
|
workflow_name: 工作流名称
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: 验证或刷新是否成功
|
bool: 验证或刷新是否成功
|
||||||
"""
|
"""
|
||||||
|
|||||||
253
unilabos/registry/devices/bioyond_dispensing_station.yaml
Normal file
253
unilabos/registry/devices/bioyond_dispensing_station.yaml
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
bioyond_dispensing_station:
|
||||||
|
category:
|
||||||
|
- workstation
|
||||||
|
- bioyond
|
||||||
|
- bioyond_dispensing_station
|
||||||
|
class:
|
||||||
|
action_value_mappings:
|
||||||
|
create_90_10_vial_feeding_task:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
delay_time: delay_time
|
||||||
|
hold_m_name: hold_m_name
|
||||||
|
order_name: order_name
|
||||||
|
percent_10_1_assign_material_name: percent_10_1_assign_material_name
|
||||||
|
percent_10_1_liquid_material_name: percent_10_1_liquid_material_name
|
||||||
|
percent_10_1_target_weigh: percent_10_1_target_weigh
|
||||||
|
percent_10_1_volume: percent_10_1_volume
|
||||||
|
percent_10_2_assign_material_name: percent_10_2_assign_material_name
|
||||||
|
percent_10_2_liquid_material_name: percent_10_2_liquid_material_name
|
||||||
|
percent_10_2_target_weigh: percent_10_2_target_weigh
|
||||||
|
percent_10_2_volume: percent_10_2_volume
|
||||||
|
percent_10_3_assign_material_name: percent_10_3_assign_material_name
|
||||||
|
percent_10_3_liquid_material_name: percent_10_3_liquid_material_name
|
||||||
|
percent_10_3_target_weigh: percent_10_3_target_weigh
|
||||||
|
percent_10_3_volume: percent_10_3_volume
|
||||||
|
percent_90_1_assign_material_name: percent_90_1_assign_material_name
|
||||||
|
percent_90_1_target_weigh: percent_90_1_target_weigh
|
||||||
|
percent_90_2_assign_material_name: percent_90_2_assign_material_name
|
||||||
|
percent_90_2_target_weigh: percent_90_2_target_weigh
|
||||||
|
percent_90_3_assign_material_name: percent_90_3_assign_material_name
|
||||||
|
percent_90_3_target_weigh: percent_90_3_target_weigh
|
||||||
|
speed: speed
|
||||||
|
temperature: temperature
|
||||||
|
goal_default:
|
||||||
|
delay_time: ''
|
||||||
|
hold_m_name: ''
|
||||||
|
order_name: ''
|
||||||
|
percent_10_1_assign_material_name: ''
|
||||||
|
percent_10_1_liquid_material_name: ''
|
||||||
|
percent_10_1_target_weigh: ''
|
||||||
|
percent_10_1_volume: ''
|
||||||
|
percent_10_2_assign_material_name: ''
|
||||||
|
percent_10_2_liquid_material_name: ''
|
||||||
|
percent_10_2_target_weigh: ''
|
||||||
|
percent_10_2_volume: ''
|
||||||
|
percent_10_3_assign_material_name: ''
|
||||||
|
percent_10_3_liquid_material_name: ''
|
||||||
|
percent_10_3_target_weigh: ''
|
||||||
|
percent_10_3_volume: ''
|
||||||
|
percent_90_1_assign_material_name: ''
|
||||||
|
percent_90_1_target_weigh: ''
|
||||||
|
percent_90_2_assign_material_name: ''
|
||||||
|
percent_90_2_target_weigh: ''
|
||||||
|
percent_90_3_assign_material_name: ''
|
||||||
|
percent_90_3_target_weigh: ''
|
||||||
|
speed: ''
|
||||||
|
temperature: ''
|
||||||
|
handles: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
title: DispenStationVialFeed_Feedback
|
||||||
|
type: object
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
delay_time:
|
||||||
|
type: string
|
||||||
|
hold_m_name:
|
||||||
|
type: string
|
||||||
|
order_name:
|
||||||
|
type: string
|
||||||
|
percent_10_1_assign_material_name:
|
||||||
|
type: string
|
||||||
|
percent_10_1_liquid_material_name:
|
||||||
|
type: string
|
||||||
|
percent_10_1_target_weigh:
|
||||||
|
type: string
|
||||||
|
percent_10_1_volume:
|
||||||
|
type: string
|
||||||
|
percent_10_2_assign_material_name:
|
||||||
|
type: string
|
||||||
|
percent_10_2_liquid_material_name:
|
||||||
|
type: string
|
||||||
|
percent_10_2_target_weigh:
|
||||||
|
type: string
|
||||||
|
percent_10_2_volume:
|
||||||
|
type: string
|
||||||
|
percent_10_3_assign_material_name:
|
||||||
|
type: string
|
||||||
|
percent_10_3_liquid_material_name:
|
||||||
|
type: string
|
||||||
|
percent_10_3_target_weigh:
|
||||||
|
type: string
|
||||||
|
percent_10_3_volume:
|
||||||
|
type: string
|
||||||
|
percent_90_1_assign_material_name:
|
||||||
|
type: string
|
||||||
|
percent_90_1_target_weigh:
|
||||||
|
type: string
|
||||||
|
percent_90_2_assign_material_name:
|
||||||
|
type: string
|
||||||
|
percent_90_2_target_weigh:
|
||||||
|
type: string
|
||||||
|
percent_90_3_assign_material_name:
|
||||||
|
type: string
|
||||||
|
percent_90_3_target_weigh:
|
||||||
|
type: string
|
||||||
|
speed:
|
||||||
|
type: string
|
||||||
|
temperature:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- order_name
|
||||||
|
- percent_90_1_assign_material_name
|
||||||
|
- percent_90_1_target_weigh
|
||||||
|
- percent_90_2_assign_material_name
|
||||||
|
- percent_90_2_target_weigh
|
||||||
|
- percent_90_3_assign_material_name
|
||||||
|
- percent_90_3_target_weigh
|
||||||
|
- percent_10_1_assign_material_name
|
||||||
|
- percent_10_1_target_weigh
|
||||||
|
- percent_10_1_volume
|
||||||
|
- percent_10_1_liquid_material_name
|
||||||
|
- percent_10_2_assign_material_name
|
||||||
|
- percent_10_2_target_weigh
|
||||||
|
- percent_10_2_volume
|
||||||
|
- percent_10_2_liquid_material_name
|
||||||
|
- percent_10_3_assign_material_name
|
||||||
|
- percent_10_3_target_weigh
|
||||||
|
- percent_10_3_volume
|
||||||
|
- percent_10_3_liquid_material_name
|
||||||
|
- speed
|
||||||
|
- temperature
|
||||||
|
- delay_time
|
||||||
|
- hold_m_name
|
||||||
|
title: DispenStationVialFeed_Goal
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
title: DispenStationVialFeed_Result
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: DispenStationVialFeed
|
||||||
|
type: object
|
||||||
|
type: DispenStationVialFeed
|
||||||
|
create_diamine_solution_task:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
delay_time: delay_time
|
||||||
|
hold_m_name: hold_m_name
|
||||||
|
liquid_material_name: liquid_material_name
|
||||||
|
material_name: material_name
|
||||||
|
order_name: order_name
|
||||||
|
speed: speed
|
||||||
|
target_weigh: target_weigh
|
||||||
|
temperature: temperature
|
||||||
|
volume: volume
|
||||||
|
goal_default:
|
||||||
|
delay_time: ''
|
||||||
|
hold_m_name: ''
|
||||||
|
liquid_material_name: ''
|
||||||
|
material_name: ''
|
||||||
|
order_name: ''
|
||||||
|
speed: ''
|
||||||
|
target_weigh: ''
|
||||||
|
temperature: ''
|
||||||
|
volume: ''
|
||||||
|
handles: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
schema:
|
||||||
|
description: ''
|
||||||
|
properties:
|
||||||
|
feedback:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
title: DispenStationSolnPrep_Feedback
|
||||||
|
type: object
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
delay_time:
|
||||||
|
type: string
|
||||||
|
hold_m_name:
|
||||||
|
type: string
|
||||||
|
liquid_material_name:
|
||||||
|
type: string
|
||||||
|
material_name:
|
||||||
|
type: string
|
||||||
|
order_name:
|
||||||
|
type: string
|
||||||
|
speed:
|
||||||
|
type: string
|
||||||
|
target_weigh:
|
||||||
|
type: string
|
||||||
|
temperature:
|
||||||
|
type: string
|
||||||
|
volume:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- order_name
|
||||||
|
- material_name
|
||||||
|
- target_weigh
|
||||||
|
- volume
|
||||||
|
- liquid_material_name
|
||||||
|
- speed
|
||||||
|
- temperature
|
||||||
|
- delay_time
|
||||||
|
- hold_m_name
|
||||||
|
title: DispenStationSolnPrep_Goal
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
title: DispenStationSolnPrep_Result
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: DispenStationSolnPrep
|
||||||
|
type: object
|
||||||
|
type: DispenStationSolnPrep
|
||||||
|
module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispensingStation
|
||||||
|
status_types: {}
|
||||||
|
type: python
|
||||||
|
config_info: []
|
||||||
|
description: ''
|
||||||
|
handles: []
|
||||||
|
icon: preparation_station.webp
|
||||||
|
init_param_schema:
|
||||||
|
config:
|
||||||
|
properties:
|
||||||
|
config:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- config
|
||||||
|
type: object
|
||||||
|
data:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
version: 1.0.0
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -22,3 +22,21 @@ BIOYOND_PolymerReactionStation_Deck:
|
|||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
registry_type: resource
|
registry_type: resource
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
|
||||||
|
YB_Deck11:
|
||||||
|
category:
|
||||||
|
- deck
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.decks:YB_Deck
|
||||||
|
type: pylabrobot
|
||||||
|
description: BIOYOND PolymerReactionStation Deck
|
||||||
|
handles: []
|
||||||
|
icon: 配液站.webp
|
||||||
|
init_param_schema: {}
|
||||||
|
registry_type: resource
|
||||||
|
version: 1.0.0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
from os import name
|
||||||
from pylabrobot.resources import Deck, Coordinate, Rotation
|
from pylabrobot.resources import Deck, Coordinate, Rotation
|
||||||
|
|
||||||
from unilabos.resources.bioyond.warehouses import bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x2, bioyond_warehouse_liquid_and_lid_handling
|
from unilabos.resources.bioyond.warehouses import bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x2, bioyond_warehouse_liquid_and_lid_handling, bioyond_warehouse_1x2x2, bioyond_warehouse_1x3x3, bioyond_warehouse_10x1x1, bioyond_warehouse_3x3x1, bioyond_warehouse_3x3x1_2, bioyond_warehouse_5x1x1
|
||||||
|
|
||||||
|
|
||||||
class BIOYOND_PolymerReactionStation_Deck(Deck):
|
class BIOYOND_PolymerReactionStation_Deck(Deck):
|
||||||
@@ -66,3 +67,60 @@ class BIOYOND_PolymerPreparationStation_Deck(Deck):
|
|||||||
|
|
||||||
for warehouse_name, warehouse in self.warehouses.items():
|
for warehouse_name, warehouse in self.warehouses.items():
|
||||||
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||||
|
|
||||||
|
class BIOYOND_YB_Deck(Deck):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str = "YB_Deck",
|
||||||
|
size_x: float = 4150,
|
||||||
|
size_y: float = 1400.0,
|
||||||
|
size_z: float = 2670.0,
|
||||||
|
category: str = "deck",
|
||||||
|
setup: bool = False
|
||||||
|
) -> None:
|
||||||
|
super().__init__(name=name, size_x=4150.0, size_y=1400.0, size_z=2670.0)
|
||||||
|
if setup:
|
||||||
|
self.setup()
|
||||||
|
|
||||||
|
def setup(self) -> None:
|
||||||
|
# 添加仓库
|
||||||
|
self.warehouses = {
|
||||||
|
"321窗口": bioyond_warehouse_1x2x2("321窗口"),
|
||||||
|
"43窗口": bioyond_warehouse_1x2x2("43窗口"),
|
||||||
|
"手动传递窗左": bioyond_warehouse_1x3x3("手动传递窗左"),
|
||||||
|
"手动传递窗右": bioyond_warehouse_1x3x3("手动传递窗右"),
|
||||||
|
"加样头堆栈左": bioyond_warehouse_10x1x1("加样头堆栈左"),
|
||||||
|
"加样头堆栈右": bioyond_warehouse_10x1x1("加样头堆栈右"),
|
||||||
|
|
||||||
|
"15ml配液堆栈左": bioyond_warehouse_3x3x1("15ml配液堆栈左"),
|
||||||
|
"母液加样右": bioyond_warehouse_3x3x1_2("母液加样右"),
|
||||||
|
"大瓶母液堆栈左": bioyond_warehouse_5x1x1("大瓶母液堆栈左"),
|
||||||
|
"大瓶母液堆栈右": bioyond_warehouse_5x1x1("大瓶母液堆栈右"),
|
||||||
|
}
|
||||||
|
# warehouse 的位置
|
||||||
|
self.warehouse_locations = {
|
||||||
|
"321窗口": Coordinate(-150.0, 158.0, 0.0),
|
||||||
|
"43窗口": Coordinate(4160.0, 158.0, 0.0),
|
||||||
|
"手动传递窗左": Coordinate(-150.0, 877.0, 0.0),
|
||||||
|
"手动传递窗右": Coordinate(4160.0, 877.0, 0.0),
|
||||||
|
"加样头堆栈左": Coordinate(385.0, 1300.0, 0.0),
|
||||||
|
"加样头堆栈右": Coordinate(2187.0, 1300.0, 0.0),
|
||||||
|
|
||||||
|
"15ml配液堆栈左": Coordinate(749.0, 355.0, 0.0),
|
||||||
|
"母液加样右": Coordinate(2152.0, 333.0, 0.0),
|
||||||
|
"大瓶母液堆栈左": Coordinate(1164.0, 676.0, 0.0),
|
||||||
|
"大瓶母液堆栈右": Coordinate(2717.0, 676.0, 0.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
for warehouse_name, warehouse in self.warehouses.items():
|
||||||
|
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||||
|
|
||||||
|
def YB_Deck(name: str) -> Deck:
|
||||||
|
by=BIOYOND_YB_Deck(name=name)
|
||||||
|
by.setup()
|
||||||
|
return by
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
|
def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
|
||||||
"""创建BioYond 4x1x2仓库"""
|
"""创建BioYond 4x1x2仓库"""
|
||||||
return warehouse_factory(
|
return warehouse_factory(
|
||||||
@@ -34,7 +35,113 @@ def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
|
|||||||
category="warehouse",
|
category="warehouse",
|
||||||
removed_positions=None
|
removed_positions=None
|
||||||
)
|
)
|
||||||
|
# 定义benyond的堆栈
|
||||||
|
def bioyond_warehouse_1x2x2(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x1x4仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=1,
|
||||||
|
num_items_y=2,
|
||||||
|
num_items_z=2,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
def bioyond_warehouse_10x1x1(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x1x4仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=10,
|
||||||
|
num_items_y=1,
|
||||||
|
num_items_z=1,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
def bioyond_warehouse_1x3x3(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x1x4仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=1,
|
||||||
|
num_items_y=3,
|
||||||
|
num_items_z=3,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
def bioyond_warehouse_2x1x3(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x1x4仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=2,
|
||||||
|
num_items_y=1,
|
||||||
|
num_items_z=3,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
def bioyond_warehouse_3x3x1(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x1x4仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=3,
|
||||||
|
num_items_z=1,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
def bioyond_warehouse_5x1x1(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x1x4仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=5,
|
||||||
|
num_items_y=1,
|
||||||
|
num_items_z=1,
|
||||||
|
dx=10.0,
|
||||||
|
dy=10.0,
|
||||||
|
dz=10.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
def bioyond_warehouse_3x3x1_2(name: str) -> WareHouse:
|
||||||
|
"""创建BioYond 4x1x4仓库"""
|
||||||
|
return warehouse_factory(
|
||||||
|
name=name,
|
||||||
|
num_items_x=3,
|
||||||
|
num_items_y=3,
|
||||||
|
num_items_z=1,
|
||||||
|
dx=12.0,
|
||||||
|
dy=12.0,
|
||||||
|
dz=12.0,
|
||||||
|
item_dx=137.0,
|
||||||
|
item_dy=96.0,
|
||||||
|
item_dz=120.0,
|
||||||
|
category="warehouse",
|
||||||
|
)
|
||||||
|
|
||||||
def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
|
def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
|
||||||
"""创建BioYond开关盖加液模块台面"""
|
"""创建BioYond开关盖加液模块台面"""
|
||||||
|
|||||||
@@ -575,16 +575,16 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w
|
|||||||
replace_info = {
|
replace_info = {
|
||||||
"plate": "plate",
|
"plate": "plate",
|
||||||
"well": "well",
|
"well": "well",
|
||||||
"tip_spot": "container",
|
"tip_spot": "tip_spot",
|
||||||
"trash": "container",
|
"trash": "trash",
|
||||||
"deck": "deck",
|
"deck": "deck",
|
||||||
"tip_rack": "container",
|
"tip_rack": "tip_rack",
|
||||||
}
|
}
|
||||||
if source in replace_info:
|
if source in replace_info:
|
||||||
return replace_info[source]
|
return replace_info[source]
|
||||||
else:
|
else:
|
||||||
print("转换pylabrobot的时候,出现未知类型", source)
|
print("转换pylabrobot的时候,出现未知类型", source)
|
||||||
return "container"
|
return source
|
||||||
|
|
||||||
def resource_plr_to_ulab_inner(d: dict, all_states: dict, child=True) -> dict:
|
def resource_plr_to_ulab_inner(d: dict, all_states: dict, child=True) -> dict:
|
||||||
r = {
|
r = {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ class ItemizedCarrier(ResourcePLR):
|
|||||||
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
|
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
|
||||||
category: Optional[str] = "carrier",
|
category: Optional[str] = "carrier",
|
||||||
model: Optional[str] = None,
|
model: Optional[str] = None,
|
||||||
|
invisible_slots: Optional[str] = None,
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name,
|
name=name,
|
||||||
@@ -89,6 +90,7 @@ class ItemizedCarrier(ResourcePLR):
|
|||||||
)
|
)
|
||||||
self.num_items = len(sites)
|
self.num_items = len(sites)
|
||||||
self.num_items_x, self.num_items_y, self.num_items_z = num_items_x, num_items_y, num_items_z
|
self.num_items_x, self.num_items_y, self.num_items_z = num_items_x, num_items_y, num_items_z
|
||||||
|
self.invisible_slots = [] if invisible_slots is None else invisible_slots
|
||||||
self.layout = "z-y" if self.num_items_z > 1 and self.num_items_x == 1 else "x-z" if self.num_items_z > 1 and self.num_items_y == 1 else "x-y"
|
self.layout = "z-y" if self.num_items_z > 1 and self.num_items_x == 1 else "x-z" if self.num_items_z > 1 and self.num_items_y == 1 else "x-y"
|
||||||
|
|
||||||
if isinstance(sites, dict):
|
if isinstance(sites, dict):
|
||||||
@@ -410,7 +412,7 @@ class ItemizedCarrier(ResourcePLR):
|
|||||||
"layout": self.layout,
|
"layout": self.layout,
|
||||||
"sites": [{
|
"sites": [{
|
||||||
"label": str(identifier),
|
"label": str(identifier),
|
||||||
"visible": True if self[identifier] is not None else False,
|
"visible": False if identifier in self.invisible_slots else True,
|
||||||
"occupied_by": self[identifier].name
|
"occupied_by": self[identifier].name
|
||||||
if isinstance(self[identifier], ResourcePLR) and not isinstance(self[identifier], ResourceHolder) else
|
if isinstance(self[identifier], ResourcePLR) and not isinstance(self[identifier], ResourceHolder) else
|
||||||
self[identifier] if isinstance(self[identifier], str) else None,
|
self[identifier] if isinstance(self[identifier], str) else None,
|
||||||
@@ -433,6 +435,7 @@ class BottleCarrier(ItemizedCarrier):
|
|||||||
sites: Optional[Dict[Union[int, str], ResourceHolder]] = None,
|
sites: Optional[Dict[Union[int, str], ResourceHolder]] = None,
|
||||||
category: str = "bottle_carrier",
|
category: str = "bottle_carrier",
|
||||||
model: Optional[str] = None,
|
model: Optional[str] = None,
|
||||||
|
invisible_slots: List[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -443,4 +446,5 @@ class BottleCarrier(ItemizedCarrier):
|
|||||||
sites=sites,
|
sites=sites,
|
||||||
category=category,
|
category=category,
|
||||||
model=model,
|
model=model,
|
||||||
|
invisible_slots=invisible_slots,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class ResourceDictPositionObject(BaseModel):
|
|||||||
class ResourceDictPosition(BaseModel):
|
class ResourceDictPosition(BaseModel):
|
||||||
size: ResourceDictPositionSize = Field(description="Resource size", default_factory=ResourceDictPositionSize)
|
size: ResourceDictPositionSize = Field(description="Resource size", default_factory=ResourceDictPositionSize)
|
||||||
scale: ResourceDictPositionScale = Field(description="Resource scale", default_factory=ResourceDictPositionScale)
|
scale: ResourceDictPositionScale = Field(description="Resource scale", default_factory=ResourceDictPositionScale)
|
||||||
layout: Literal["2d", "x-y", "z-y", "x-z"] = Field(description="Resource layout", default="x-y")
|
layout: Literal["2d", "x-y", "z-y", "x-z", ""] = Field(description="Resource layout", default="x-y")
|
||||||
position: ResourceDictPositionObject = Field(
|
position: ResourceDictPositionObject = Field(
|
||||||
description="Resource position", default_factory=ResourceDictPositionObject
|
description="Resource position", default_factory=ResourceDictPositionObject
|
||||||
)
|
)
|
||||||
@@ -42,7 +42,7 @@ class ResourceDictPosition(BaseModel):
|
|||||||
rotation: ResourceDictPositionObject = Field(
|
rotation: ResourceDictPositionObject = Field(
|
||||||
description="Resource rotation", default_factory=ResourceDictPositionObject
|
description="Resource rotation", default_factory=ResourceDictPositionObject
|
||||||
)
|
)
|
||||||
cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field(description="Cross section type", default="rectangle")
|
cross_section_type: Literal["rectangle", "circle", "rounded_rectangle", ""] = Field(description="Cross section type", default="rectangle")
|
||||||
|
|
||||||
|
|
||||||
# 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化
|
# 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化
|
||||||
@@ -311,12 +311,16 @@ class ResourceTreeSet(object):
|
|||||||
"plate": "plate",
|
"plate": "plate",
|
||||||
"well": "well",
|
"well": "well",
|
||||||
"deck": "deck",
|
"deck": "deck",
|
||||||
|
"tip_rack": "tip_rack",
|
||||||
|
"tip_spot": "tip_spot",
|
||||||
|
"tube": "tube",
|
||||||
|
"bottle_carrier": "bottle_carrier",
|
||||||
}
|
}
|
||||||
if source in replace_info:
|
if source in replace_info:
|
||||||
return replace_info[source]
|
return replace_info[source]
|
||||||
else:
|
else:
|
||||||
print("转换pylabrobot的时候,出现未知类型", source)
|
print("转换pylabrobot的时候,出现未知类型", source)
|
||||||
return "container"
|
return source
|
||||||
|
|
||||||
def build_uuid_mapping(res: "PLRResource", uuid_list: list):
|
def build_uuid_mapping(res: "PLRResource", uuid_list: list):
|
||||||
"""递归构建uuid映射字典"""
|
"""递归构建uuid映射字典"""
|
||||||
@@ -402,7 +406,7 @@ class ResourceTreeSet(object):
|
|||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
# 类型映射
|
# 类型映射
|
||||||
TYPE_MAP = {"plate": "Plate", "well": "Well", "deck": "Deck"}
|
TYPE_MAP = {"plate": "Plate", "well": "Well", "deck": "Deck", "container": "RegularContainer"}
|
||||||
|
|
||||||
def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict):
|
def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict):
|
||||||
"""一次遍历收集 name_to_uuid 和 all_states"""
|
"""一次遍历收集 name_to_uuid 和 all_states"""
|
||||||
|
|||||||
Reference in New Issue
Block a user