# 实验图高级参考 本文件是 SKILL.md 的补充,包含 ResourceDict 完整 schema、Handle 验证、GraphML 格式、Pose 标准化规则和复杂图文件结构。Agent 在需要处理这些场景时按需阅读。 --- ## 1. ResourceDict 完整字段 `unilabos/resources/resource_tracker.py` 中定义的节点数据模型: | 字段 | 类型 | 别名 | 说明 | |------|------|------|------| | `id` | `str` | — | 节点唯一标识 | | `uuid` | `str` | — | 全局唯一标识 | | `name` | `str` | — | 显示名称 | | `description` | `str` | — | 描述(默认 `""` ) | | `resource_schema` | `Dict[str, Any]` | `schema` | 资源 schema | | `model` | `Dict[str, Any]` | — | 3D 模型信息 | | `icon` | `str` | — | 图标(默认 `""` ) | | `parent_uuid` | `Optional[str]` | — | 父节点 UUID | | `parent` | `Optional[ResourceDict]` | — | 父节点引用(序列化时 exclude) | | `type` | `Union[Literal["device"], str]` | — | 节点类型 | | `klass` | `str` | `class` | 注册表类名 | | `pose` | `ResourceDictPosition` | — | 位姿信息 | | `config` | `Dict[str, Any]` | — | 配置参数 | | `data` | `Dict[str, Any]` | — | 运行时数据 | | `extra` | `Dict[str, Any]` | — | 扩展数据 | ### Pose 完整结构(ResourceDictPosition) | 字段 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `size` | `{width, height, depth}` | `{0,0,0}` | 节点尺寸 | | `scale` | `{x, y, z}` | `{1,1,1}` | 缩放比例 | | `layout` | `"2d"/"x-y"/"z-y"/"x-z"` | `"x-y"` | 布局方向 | | `position` | `{x, y, z}` | `{0,0,0}` | 2D 位置 | | `position3d` | `{x, y, z}` | `{0,0,0}` | 3D 位置 | | `rotation` | `{x, y, z}` | `{0,0,0}` | 旋转角度 | | `cross_section_type` | `"rectangle"/"circle"/"rounded_rectangle"` | `"rectangle"` | 横截面形状 | --- ## 2. Position / Pose 标准化规则 图文件中的 `position` 有多种写法,加载时自动标准化。 ### 输入格式兼容 ```json // 格式 A: 直接 {x, y, z}(最常用) "position": {"x": 100, "y": 200, "z": 0} // 格式 B: 嵌套 position "position": {"position": {"x": 100, "y": 200, "z": 0}} // 格式 C: 使用 pose 字段 "pose": {"position": {"x": 100, "y": 200, "z": 0}} // 格式 D: 顶层 x, y, z(无 position 字段) "x": 100, "y": 200, "z": 0 ``` ### 标准化流程 1. **graphio.py `canonicalize_nodes_data`**:若 `position` 不是 dict,从节点顶层提取 `x/y/z` 填入 `pose.position` 2. **resource_tracker.py `get_resource_instance_from_dict`**:若 `position.x` 存在(旧格式),转为 `{"position": {"x":..., "y":..., "z":...}}` 3. `pose.size` 从 `config.size_x/size_y/size_z` 自动填充 --- ## 3. Handle 验证 启动时系统验证 link 中的 `sourceHandle` / `targetHandle` 是否在注册表的 `handles` 中定义。 ```python # unilabos/app/main.py (约 449-481 行) source_handler_keys = [ h["handler_key"] for h in materials[source_node.klass]["handles"] if h["io_type"] == "source" ] target_handler_keys = [ h["handler_key"] for h in materials[target_node.klass]["handles"] if h["io_type"] == "target" ] if source_handle not in source_handler_keys: print_status(f"节点 {source_node.id} 的source端点 {source_handle} 不存在", "error") resource_edge_info.pop(...) # 移除非法 link ``` **Handle 定义在注册表 YAML 中:** ```yaml my_device: handles: - handler_key: access io_type: target data_type: fluid side: NORTH label: access ``` > 大多数简单设备不定义 handles,此验证仅对有 `sourceHandle`/`targetHandle` 的 link 生效。 --- ## 4. GraphML 格式支持 除 JSON 外,系统也支持 GraphML 格式(`unilabos/resources/graphio.py::read_graphml`)。 ### 与 JSON 的关键差异 | 特性 | JSON | GraphML | |------|------|---------| | 父子关系 | `parent`/`children` 字段 | `::` 分隔的节点 ID(如 `station::pump_1`) | | 加载后 | 直接解析 | 先 `nx.read_graphml` 再转 JSON 格式 | | 输出 | 不生成副本 | 自动生成等价的 `.json` 文件 | ### GraphML 转换流程 ``` nx.read_graphml(file) ↓ 用 label 重映射节点名 ↓ 从 "::" 推断 parent_relation nx.relabel_nodes + nx.node_link_data ↓ canonicalize_nodes_data + canonicalize_links_ports ↓ 写出等价 JSON 文件 physical_setup_graph + handle_communications ``` --- ## 5. 复杂图文件结构示例 ### 外部系统工作站完整 config 以 `reaction_station_bioyond.json` 为例,工作站 `config` 中的关键字段: ```json { "config": { "api_key": "DE9BDDA0", "api_host": "http://172.21.103.36:45388", "workflow_mappings": { "scheduler_start": {"workflow": "start", "params": {}}, "create_order": {"workflow": "create_order", "params": {}} }, "material_type_mappings": { "BIOYOND_PolymerStation_Reactor": ["反应器", "type-uuid-here"], "BIOYOND_PolymerStation_1BottleCarrier": ["试剂瓶", "type-uuid-here"] }, "warehouse_mapping": { "堆栈1左": { "uuid": "warehouse-uuid-here", "site_uuids": { "A01": "site-uuid-1", "A02": "site-uuid-2" } } }, "http_service_config": { "enabled": true, "host": "0.0.0.0", "port": 45399, "routes": ["/callback/workflow", "/callback/material"] }, "deck": { "data": { "_resource_child_name": "Bioyond_Deck", "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck" } }, "size_x": 2700.0, "size_y": 1080.0, "size_z": 2500.0, "protocol_type": [], "data": {} } } ``` ### 子设备 Reactor 节点 ```json { "id": "reactor_1", "name": "reactor_1", "parent": "reaction_station_bioyond", "type": "device", "class": "bioyond_reactor", "position": {"x": 1150, "y": 300, "z": 0}, "config": { "reactor_index": 0, "bioyond_workflow_key": "reactor_1" }, "data": {} } ``` ### Deck 节点 ```json { "id": "Bioyond_Deck", "name": "Bioyond_Deck", "parent": "reaction_station_bioyond", "type": "deck", "class": "BIOYOND_PolymerReactionStation_Deck", "position": {"x": 0, "y": 0, "z": 0}, "config": { "type": "BIOYOND_PolymerReactionStation_Deck", "setup": true, "rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"} }, "data": {} } ``` --- ## 6. Link 端口标准化 `graphio.py::canonicalize_links_ports` 处理 `port` 字段的多种格式: ```python # 输入: 字符串格式 "(A,B)" "port": "(pump_1, valve_1)" # 输出: 字典格式 "port": {"source_id": "pump_1", "target_id": "valve_1"} # 输入: 已是字典 "port": {"pump_1": "port", "serial_1": "port"} # 保持不变 # 输入: 无 port 字段 # 自动补充空 port ``` --- ## 7. 关键路径 | 内容 | 路径 | |------|------| | ResourceDict 模型 | `unilabos/resources/resource_tracker.py` | | 图加载 + 标准化 | `unilabos/resources/graphio.py` | | Handle 验证 | `unilabos/app/main.py` (449-481 行) | | 反应站图文件 | `unilabos/test/experiments/reaction_station_bioyond.json` | | 配液站图文件 | `unilabos/test/experiments/dispensing_station_bioyond.json` | | 用户文档 | `docs/user_guide/graph_files.md` |