Compare commits

...

2 Commits

Author SHA1 Message Date
Guangxin Zhang
573c724a5c 新增版位推荐功能 2025-09-17 21:07:19 +01:00
Xuwznln
09549d2839 resource_update use resource_add 2025-09-18 03:47:26 +08:00
8 changed files with 414 additions and 229 deletions

View File

@@ -13,18 +13,16 @@
```json ```json
{ {
"nodes": [ "nodes": [
{ {
"id": "PLR_STATION", "id": "PLR_STATION",
"name": "PLR_LH_TEST", "name": "PLR_LH_TEST",
"parent": null, "parent": null,
"type": "device", "type": "device",
"class": "liquid_handler", "class": "liquid_handler",
"config": {}, "config": {},
"data": {}, "data": {},
"children": [ "children": ["deck"]
"deck" },
]
},
{ {
"id": "deck", "id": "deck",
"name": "deck", "name": "deck",
@@ -32,12 +30,12 @@
"class": null, "class": null,
"parent": "PLR_STATION", "parent": "PLR_STATION",
"children": [ "children": [
"trash", "trash",
"trash_core96", "trash_core96",
"teaching_carrier", "teaching_carrier",
"tip_rack", "tip_rack",
"plate" "plate"
] ]
} }
], ],
"links": [] "links": []
@@ -45,6 +43,7 @@
``` ```
配置文件定义了移液站的组成部分,主要包括: 配置文件定义了移液站的组成部分,主要包括:
- 移液站本体LiquidHandler- 设备类型 - 移液站本体LiquidHandler- 设备类型
- 移液站携带物料实例deck- 物料类型 - 移液站携带物料实例deck- 物料类型
@@ -55,7 +54,7 @@
使用以下命令启动移液站设备: 使用以下命令启动移液站设备:
```bash ```bash
unilab -g test/experiments/plr_test.json --app_bridges "" unilab -g test/experiments/plr_test.json --ak [通过网页获取的ak值] --sk [通过网页获取的sk值]
``` ```
### 2. 执行枪头插入操作 ### 2. 执行枪头插入操作
@@ -66,35 +65,50 @@ unilab -g test/experiments/plr_test.json --app_bridges ""
ros2 action send_goal /devices/PLR_STATION/pick_up_tips unilabos_msgs/action/_liquid_handler_pick_up_tips/LiquidHandlerPickUpTips "{ tip_spots: [ { id: 'tip_rack_tipspot_0_0', name: 'tip_rack_tipspot_0_0', sample_id: null, children: [], parent: 'tip_rack', type: 'device', config: { position: { x: 7.2, y: 68.3, z: -83.5 }, size_x: 9.0, size_y: 9.0, size_z: 0, rotation: { x: 0, y: 0, z: 0, type: 'Rotation' }, category: 'tip_spot', model: null, type: 'TipSpot', prototype_tip: { type: 'HamiltonTip', total_tip_length: 95.1, has_filter: true, maximal_volume: 1065, pickup_method: 'OUT_OF_RACK', tip_size: 'HIGH_VOLUME' } }, data: { tip: { type: 'HamiltonTip', total_tip_length: 95.1, has_filter: true, maximal_volume: 1065, pickup_method: 'OUT_OF_RACK', tip_size: 'HIGH_VOLUME' }, tip_state: { liquids: [], pending_liquids: [], liquid_history: [] }, pending_tip: { type: 'HamiltonTip', total_tip_length: 95.1, has_filter: true, maximal_volume: 1065, pickup_method: 'OUT_OF_RACK', tip_size: 'HIGH_VOLUME' } } } ], use_channels: [ 0 ], offsets: [ { x: 0.0, y: 0.0, z: 0.0 } ] }" ros2 action send_goal /devices/PLR_STATION/pick_up_tips unilabos_msgs/action/_liquid_handler_pick_up_tips/LiquidHandlerPickUpTips "{ tip_spots: [ { id: 'tip_rack_tipspot_0_0', name: 'tip_rack_tipspot_0_0', sample_id: null, children: [], parent: 'tip_rack', type: 'device', config: { position: { x: 7.2, y: 68.3, z: -83.5 }, size_x: 9.0, size_y: 9.0, size_z: 0, rotation: { x: 0, y: 0, z: 0, type: 'Rotation' }, category: 'tip_spot', model: null, type: 'TipSpot', prototype_tip: { type: 'HamiltonTip', total_tip_length: 95.1, has_filter: true, maximal_volume: 1065, pickup_method: 'OUT_OF_RACK', tip_size: 'HIGH_VOLUME' } }, data: { tip: { type: 'HamiltonTip', total_tip_length: 95.1, has_filter: true, maximal_volume: 1065, pickup_method: 'OUT_OF_RACK', tip_size: 'HIGH_VOLUME' }, tip_state: { liquids: [], pending_liquids: [], liquid_history: [] }, pending_tip: { type: 'HamiltonTip', total_tip_length: 95.1, has_filter: true, maximal_volume: 1065, pickup_method: 'OUT_OF_RACK', tip_size: 'HIGH_VOLUME' } } } ], use_channels: [ 0 ], offsets: [ { x: 0.0, y: 0.0, z: 0.0 } ] }"
``` ```
此命令会通过ros通信触发移液站执行枪头插入操作得到如下的PyLabRobot的输出日志。 此命令会通过 ros 通信触发移液站执行枪头插入操作,得到如下的 PyLabRobot 的输出日志。
```log ```log
Picking up tips: Picking up tips:
pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter
p0: tip_rack_tipspot_0_0 0.0,0.0,0.0 HamiltonTip 1065 8 95.1 Yes p0: tip_rack_tipspot_0_0 0.0,0.0,0.0 HamiltonTip 1065 8 95.1 Yes
``` ```
也可以登陆网页,给`tip_spots`选择`tip_rack_tipspot_0_0``use_channels``0``offsets`均填写`0`,同样可观察到上面的日志
## 常见问题 ## 常见问题
1. **重复插入枪头不成功**操作编排应该符合实际操作顺序可自行通过PyLabRobot进行测试 1. **重复插入枪头不成功**:操作编排应该符合实际操作顺序,可自行通过 PyLabRobot 进行测试
## 移液站支持的操作 ## 移液站支持的操作
移液站支持多种操作,以下是当前系统支持的操作列表: 移液站支持多种操作,以下是当前系统支持的操作列表:
1. **LiquidHandlerAspirate** - 吸液操作 1. **LiquidHandlerProtocolCreation** - 协议创建
2. **LiquidHandlerDispense** - 液操作 2. **LiquidHandlerAspirate** - 液操作
3. **LiquidHandlerDiscardTips** - 丢弃枪头 3. **LiquidHandlerDispense** - 排液操作
4. **LiquidHandlerDropTips** - 卸下枪头 4. **LiquidHandlerDiscardTips** - 丢弃枪头
5. **LiquidHandlerDropTips96** - 卸下96通道枪头 5. **LiquidHandlerDropTips** - 卸下枪头
6. **LiquidHandlerMoveLid** - 移动盖子 6. **LiquidHandlerDropTips96** - 卸下 96 通道枪头
7. **LiquidHandlerMovePlate** - 移动 7. **LiquidHandlerMoveLid** - 移动
8. **LiquidHandlerMoveResource** - 移动资源 8. **LiquidHandlerMovePlate** - 移动板子
9. **LiquidHandlerPickUpTips** - 插入枪头 9. **LiquidHandlerMoveResource** - 移动资源
10. **LiquidHandlerPickUpTips96** - 插入96通道枪头 10. **LiquidHandlerPickUpTips** - 插入枪头
11. **LiquidHandlerReturnTips** - 归还枪头 11. **LiquidHandlerPickUpTips96** - 插入 96 通道枪头
12. **LiquidHandlerReturnTips96** - 归还96通道枪头 12. **LiquidHandlerReturnTips** - 归还枪头
13. **LiquidHandlerStamp** - 打印标记 13. **LiquidHandlerReturnTips96** - 归还 96 通道枪头
14. **LiquidHandlerTransfer** - 液体转移 14. **LiquidHandlerSetLiquid** - 设置液体
15. **LiquidHandlerSetTipRack** - 设置枪头架
16. **LiquidHandlerStamp** - 打印标记
17. **LiquidHandlerTransfer** - 液体转移
18. **LiquidHandlerSetGroup** - 设置分组
19. **LiquidHandlerTransferBiomek** - Biomek 液体转移
20. **LiquidHandlerIncubateBiomek** - Biomek 孵育
21. **LiquidHandlerMoveBiomek** - Biomek 移动
22. **LiquidHandlerOscillateBiomek** - Biomek 振荡
23. **LiquidHandlerTransferGroup** - 分组转移
24. **LiquidHandlerAdd** - 添加操作
25. **LiquidHandlerMix** - 混合操作
26. **LiquidHandlerMoveTo** - 移动到指定位置
27. **LiquidHandlerRemove** - 移除操作
这些操作可通过ROS2 Action接口进行调用以实现复杂的移液流程。 这些操作可通过 ROS2 Action 接口进行调用,以实现复杂的移液流程。

View File

@@ -13,36 +13,36 @@ class MockGripper:
self._velocity: float = 2.0 self._velocity: float = 2.0
self._torque: float = 0.0 self._torque: float = 0.0
self._status = "Idle" self._status = "Idle"
@property @property
def position(self) -> float: def position(self) -> float:
return self._position return self._position
@property @property
def velocity(self) -> float: def velocity(self) -> float:
return self._velocity return self._velocity
@property @property
def torque(self) -> float: def torque(self) -> float:
return self._torque return self._torque
# 会被自动识别的设备属性,接入 Uni-Lab 时会定时对外广播 # 会被自动识别的设备属性,接入 Uni-Lab 时会定时对外广播
@property @property
def status(self) -> str: def status(self) -> str:
return self._status return self._status
# 会被自动识别的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令 # 会被自动识别的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
@status.setter @status.setter
def status(self, target): def status(self, target):
self._status = target self._status = target
# 需要在注册表添加的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令 # 需要在注册表添加的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
def push_to(self, position: float, torque: float, velocity: float = 0.0): def push_to(self, position: float, torque: float, velocity: float = 0.0):
self._status = "Running" self._status = "Running"
current_pos = self.position current_pos = self.position
if velocity == 0.0: if velocity == 0.0:
velocity = self.velocity velocity = self.velocity
move_time = abs(position - current_pos) / velocity move_time = abs(position - current_pos) / velocity
for i in range(20): for i in range(20):
self._position = current_pos + (position - current_pos) / 20 * (i+1) self._position = current_pos + (position - current_pos) / 20 * (i+1)
@@ -68,7 +68,7 @@ public class MockGripper
public double velocity { get; private set; } = 2.0; public double velocity { get; private set; } = 2.0;
public double torque { get; private set; } = 0.0; public double torque { get; private set; } = 0.0;
public string status { get; private set; } = "Idle"; public string status { get; private set; } = "Idle";
// 需要在注册表添加的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令 // 需要在注册表添加的设备动作,接入 Uni-Lab 时会作为 ActionServer 接受任意控制者的指令
public async Task PushToAsync(double Position, double Torque, double Velocity = 0.0) public async Task PushToAsync(double Position, double Torque, double Velocity = 0.0)
{ {
@@ -94,107 +94,61 @@ public class MockGripper
C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能使用,但只需一次。 C# 驱动设备在完成注册表后,需要调用 Uni-Lab C# 编译后才能使用,但只需一次。
## 注册表文件位置 ## 快速开始:使用注册表编辑器(推荐)
Uni-Lab 启动时会自动读取默认注册表路径 `unilabos/registry/devices` 下的所有注册设备。您也可以任意维护自己的注册表路径,只需要在 Uni-Lab 启动时使用 `--registry` 参数将路径添加即可。 推荐使用 Uni-Lab-OS 自带的可视化编辑器,它能自动分析您的设备驱动并生成大部分配置:
`<path-to-registry>/devices` 中新建一个 yaml 文件,即可开始撰写。您可以将多个设备写到同一个 yaml 文件中。 1. 启动 Uni-Lab-OS
2. 在浏览器中打开"注册表编辑器"页面
3. 选择您的 Python 设备驱动文件
4. 点击"分析文件",让系统读取类信息
5. 填写基本信息(设备描述、图标等)
6. 点击"生成注册表",复制生成的内容
7. 保存到 `devices/` 目录下
## 注册表的结构 ---
1. 顶层名称:每个设备的注册表以设备名称开头,例如 `new_device`, `gripper.mock` ## 手动编写注册表(简化版)
1. `class` 字段:定义设备的模块路径和驱动程序语言。
1. `status_types` 字段:定义设备定时对 Uni-Lab 实验室内发送的属性名及其类型。
1. `action_value_mappings` 字段:定义设备支持的动作及其目标、反馈和结果。
1. `schema` 字段:定义设备定时对 Uni-Lab 云端监控发送的属性名及其类型、描述(非必须)
## 创建新的注册表教程 如果需要手动编写,只需要提供两个必需字段,系统会自动补全其余内容:
1. 创建文件 ### 最小配置示例
在 devices 文件夹中创建一个新的 YAML 文件,例如 `new_device.yaml`
2. 定义设备名称
在文件中定义设备的顶层名称,例如:`new_device``gripper.mock`
3. 定义设备的类信息
添加设备的模块路径和类型:
```yaml ```yaml
gripper.mock: my_device: # 设备唯一标识符
class: # 定义设备的类信息 class:
module: unilabos.devices.gripper.mock:MockGripper module: unilabos.devices.your_module.my_device:MyDevice # Python 类路径
type: python # 指定驱动语言为 Python type: python # 驱动类型
status_types:
position: Float64
torque: Float64
status: String
``` ```
4. 定义设备的定时发布属性。注意,对于 Python Class 来说PROP 是 class 的 `property`,或满足能被 `getattr(cls, PROP)``cls.get_PROP` 读取到的属性值的对象。 ### 注册表文件位置
- 默认路径:`unilabos/registry/devices`
- 自定义路径:启动时使用 `--registry` 参数指定
- 可将多个设备写在同一个 yaml 文件中
### 系统自动生成的内容
系统会自动分析您的 Python 驱动类并生成:
- `status_types`:从 `get_*` 方法自动识别状态属性
- `action_value_mappings`:从类方法自动生成动作映射
- `init_param_schema`:从 `__init__` 方法分析初始化参数
- `schema`:前端显示用的属性类型定义
### 完整结构概览
```yaml ```yaml
status_types: my_device:
PROP: TYPE class:
``` module: unilabos.devices.your_module.my_device:MyDevice
5. 定义设备支持的动作 type: python
添加设备支持的动作及其目标、反馈和结果: status_types: {} # 自动生成
action_value_mappings: {} # 自动生成
```yaml description: '' # 可选:设备描述
action_value_mappings: icon: '' # 可选:设备图标
set_speed: init_param_schema: {} # 自动生成
type: SendCmd schema: {} # 自动生成
goal:
command: speed
feedback: {}
result:
success: success
``` ```
在 devices 文件夹中的 YAML 文件中action_value_mappings 是用来将驱动内的动作函数,映射到 Uni-Lab 标准动作actions及其目标参数值goal、反馈值feedback和结果值result的映射规则。若在 Uni-Lab 指令集内找不到符合心意的,请【创建新指令】 详细的注册表编写指南和高级配置,请参考{doc}`yaml 注册表编写指南 <add_yaml>`
```yaml
action_value_mappings:
<action_name>: # <action_name>:动作的名称
# start启动设备或某个功能。
# stop停止设备或某个功能。
# set_speed设置设备的速度。
# set_temperature设置设备的温度。
# move_to_position移动设备到指定位置。
# stir执行搅拌操作。
# heatchill执行加热或冷却操作。
# send_nav_task发送导航任务例如机器人导航
# set_timer设置设备的计时器。
# valve_open_cmd打开阀门。
# valve_close_cmd关闭阀门。
# execute_command_from_outer执行外部命令。
# push_to控制设备推送到某个位置例如机械爪
# move_through_points导航设备通过多个点。
type: <ActionType> # 动作的类型,表示动作的功能
# 根据动作的功能选择合适的类型,请查阅 Uni-Lab 已支持的指令集。
goal: # 定义动作的目标值映射,表示需要传递给设备的参数。
<goal_key>: <mapped_value> #确定设备需要的输入参数,并将其映射到设备的字段。
feedback: # 定义动作的反馈值映射,表示设备执行动作时返回的实时状态。
<feedback_key>: <mapped_value>
result: # 定义动作的结果值映射,表示动作完成后返回的最终结果。
<result_key>: <mapped_value>
```
6. 定义设备的网页展示属性类型,这部分会被用于在 Uni-Lab 网页端渲染成状态监控
添加设备的属性模式,包括属性类型和描述:
```yaml
schema:
type: object
properties:
status:
type: string
description: The status of the device
speed:
type: number
description: The speed of the device
required:
- status
- speed
additionalProperties: false
```

View File

@@ -1,24 +1,26 @@
# **Uni-Lab 安装** # **Uni-Lab 安装**
请先 `git clone` 本仓库,随后按照以下步骤安装项目: ## 快速开始
`Uni-Lab` 建议您采用 `mamba` 管理环境。若需从头建立 `Uni-Lab` 的运行依赖环境,请执行 1. **配置 Conda 环境**
Uni-Lab-OS 建议使用 `mamba` 管理环境。创建新的环境:
```shell ```shell
mamba env create -f unilabos-<YOUR_OS>.yaml mamba create -n unilab uni-lab::unilabos -c robostack-staging -c conda-forge
mamba activate unilab
``` ```
其中 `YOUR_OS` 是您的操作系统,可选值 `win64`, `linux-64`, `osx-64`, `osx-arm64` 2. **安装开发版 Uni-Lab-OS**
若需将依赖安装进当前环境,请执行
```shell ```shell
conda env update --file unilabos-<YOUR_OS>.yml # 配置好conda环境后克隆仓库
``` git clone https://github.com/dptech-corp/Uni-Lab-OS.git
cd Uni-Lab-OS
随后,可在本仓库安装 `unilabos` 的开发版: # 安装 Uni-Lab-OS
```shell
pip install . pip install .
``` ```
3. **启动 Uni-Lab 系统**
请参见{doc}`启动样例 <../boot_examples/index>`或{doc}`启动指南 <launch>`了解详细的启动方法。

View File

@@ -131,6 +131,7 @@ class HTTPClient:
Returns: Returns:
Response: API响应对象 Response: API响应对象
""" """
return self.resource_add(resources)
response = requests.patch( response = requests.patch(
f"{self.remote_addr}/lab/resource/batch_update/?edge_format=1", f"{self.remote_addr}/lab/resource/batch_update/?edge_format=1",
json=resources, json=resources,

View File

@@ -593,6 +593,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
available_tips[idx] = tipSpot available_tips[idx] = tipSpot
continue continue
# 一般移动液体有两种方式,一对多和多对多 # 一般移动液体有两种方式,一对多和多对多
print("channel_num", self.channel_num)
if self.channel_num == 8: if self.channel_num == 8:
tip_prefix = list(available_tips.values())[0].name.split('_')[0] tip_prefix = list(available_tips.values())[0].name.split('_')[0]
@@ -601,8 +602,11 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
available_cols.sort() available_cols.sort()
available_tips_dict = {tip.name: tip for tip in available_tips.values()} available_tips_dict = {tip.name: tip for tip in available_tips.values()}
tips_to_use = [available_tips_dict[f"{tip_prefix}_{chr(65 + i)}{available_cols[0]}"] for i in range(8)] tips_to_use = [available_tips_dict[f"{tip_prefix}_{chr(65 + i)}{available_cols[0]}"] for i in range(8)]
print("tips_to_use", tips_to_use)
await self.pick_up_tips(tips_to_use, use_channels=list(range(0, 8))) await self.pick_up_tips(tips_to_use, use_channels=list(range(0, 8)))
print("source_wells", source_wells)
await self.aspirate(source_wells, [unit_volume] * 8, use_channels=list(range(0, 8))) await self.aspirate(source_wells, [unit_volume] * 8, use_channels=list(range(0, 8)))
print("target_wells", target_wells)
await self.dispense(target_wells, [unit_volume] * 8, use_channels=list(range(0, 8))) await self.dispense(target_wells, [unit_volume] * 8, use_channels=list(range(0, 8)))
await self.discard_tips(use_channels=list(range(0, 8))) await self.discard_tips(use_channels=list(range(0, 8)))
@@ -610,7 +614,10 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
for num_well in range(len(target_wells)): for num_well in range(len(target_wells)):
tip_to_use = available_tips[list(available_tips.keys())[num_well]] tip_to_use = available_tips[list(available_tips.keys())[num_well]]
print("tip_to_use", tip_to_use)
await self.pick_up_tips([tip_to_use], use_channels=[0]) await self.pick_up_tips([tip_to_use], use_channels=[0])
print("source_wells", source_wells)
print("target_wells", target_wells)
if len(source_wells) == 1: if len(source_wells) == 1:
await self.aspirate([source_wells[0]], [unit_volume], use_channels=[0]) await self.aspirate([source_wells[0]], [unit_volume], use_channels=[0])
else: else:

View File

@@ -5,7 +5,7 @@ import json
import socket import socket
import time import time
from typing import Any, List, Dict, Optional, TypedDict, Union, Sequence, Iterator, Literal from typing import Any, List, Dict, Optional, TypedDict, Union, Sequence, Iterator, Literal
import pprint as pp
from pylabrobot.liquid_handling import ( from pylabrobot.liquid_handling import (
LiquidHandlerBackend, LiquidHandlerBackend,
Pickup, Pickup,
@@ -25,6 +25,7 @@ from pylabrobot.liquid_handling.standard import (
ResourceDrop, ResourceDrop,
) )
from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash
from traitlets import Int
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract
@@ -445,10 +446,11 @@ class LabResource:
from typing import Dict, Any from typing import Dict, Any
import time
class DefaultLayout: class DefaultLayout:
def __init__(self, product_name: str = "PRCXI9300"): def __init__(self, product_name: str = "PRCXI9300"):
self.labresource = None self.labresource = {}
if product_name not in ["PRCXI9300", "PRCXI9320"]: if product_name not in ["PRCXI9300", "PRCXI9320"]:
raise ValueError(f"Unsupported product_name: {product_name}. Only 'PRCXI9300' and 'PRCXI9320' are supported.") raise ValueError(f"Unsupported product_name: {product_name}. Only 'PRCXI9300' and 'PRCXI9320' are supported.")
@@ -458,12 +460,32 @@ class DefaultLayout:
self.layout = [1, 2, 3, 4, 5, 6] self.layout = [1, 2, 3, 4, 5, 6]
self.trash_slot = 3 self.trash_slot = 3
self.waste_liquid_slot = 6 self.waste_liquid_slot = 6
elif product_name == "PRCXI9320": elif product_name == "PRCXI9320":
self.rows = 3 self.rows = 3
self.columns = 4 self.columns = 4
self.layout = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] self.layout = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
self.trash_slot = 3 self.trash_slot = 16
self.waste_liquid_slot = 12 self.waste_liquid_slot = 12
self.default_layout = {"MatrixId":f"{time.time()}","MatrixName":f"{time.time()}","MatrixCount":16,"WorkTablets":
[{"Number": 1, "Code": "T1", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 2, "Code": "T2", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 3, "Code": "T3", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 4, "Code": "T4", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 5, "Code": "T5", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 6, "Code": "T6", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 7, "Code": "T7", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 8, "Code": "T8", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 9, "Code": "T9", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 10, "Code": "T10", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 11, "Code": "T11", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 12, "Code": "T12", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}}, # 这个设置成废液槽,用储液槽表示
{"Number": 13, "Code": "T13", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 14, "Code": "T14", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 15, "Code": "T15", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 16, "Code": "T16", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}} # 这个设置成垃圾桶,用储液槽表示
]
}
def get_layout(self) -> Dict[str, Any]: def get_layout(self) -> Dict[str, Any]:
return { return {
@@ -479,90 +501,121 @@ class DefaultLayout:
def get_waste_liquid_slot(self) -> int: def get_waste_liquid_slot(self) -> int:
return self.waste_liquid_slot return self.waste_liquid_slot
def set_liquid_handler_layout(self, product_name: str):
if product_name == "PRCXI9300":
self.rows = 2
self.columns = 3
self.layout = [1, 2, 3, 4, 5, 6]
self.trash_slot = 3
self.waste_liquid_slot = 6
elif product_name == "PRCXI9320": def add_lab_resource(self, material_info):
self.rows = 3 self.labresource = material_info
self.columns = 4
self.layout = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
self.trash_slot = 3
self.waste_liquid_slot = 12
def set_trash_slot(self, slot: int): def recommend_layout(self, needs: Dict[str, int]) -> Dict[str, Any]:
self.trash_slot = slot
def set_waste_liquid_slot(self, slot: int):
self.waste_liquid_slot = slot
def add_lab_resource(self, lab_resource: LabResource):
self.labresource = lab_resource.get_resources_info()
def recommend_layout(self, needs: Dict[str, Any]) -> Dict[str, Any]:
"""根据 needs 推荐布局""" """根据 needs 推荐布局"""
liquid_info = needs['liquid_setup'] for k, v in needs.items():
tip_info = needs['totals_by_tip'] # 修改这里:直接访问 totals_by_tip if k not in self.labresource:
print("当前实验所需物料信息:", liquid_info) raise ValueError(f"Material {k} not found in lab resources.")
print("当前实验所需枪头信息:", tip_info)
print(self.labresource) # 预留位置12和16不动
reserved_positions = {12, 16}
available_positions = [i for i in range(1, 17) if i not in reserved_positions]
# 计算总需求
total_needed = sum(needs.values())
if total_needed > len(available_positions):
raise ValueError(f"需要 {total_needed} 个位置,但只有 {len(available_positions)} 个可用位置排除位置12和16")
# 依次分配位置
current_pos = 0
for material_name, count in needs.items():
material_uuid = self.labresource[material_name]['uuid']
material_enum = self.labresource[material_name]['materialEnum']
for _ in range(count):
if current_pos >= len(available_positions):
raise ValueError("位置不足,无法分配更多物料")
position = available_positions[current_pos]
# 找到对应的tablet并更新
for tablet in self.default_layout['WorkTablets']:
if tablet['Number'] == position:
tablet['Material']['uuid'] = material_uuid
tablet['Material']['materialEnum'] = material_enum
break
current_pos += 1
return self.default_layout
for liquid in liquid_info:
# total_volume = liquid.values()
print(liquid)
#print(f"资源 {liquid} 需要的总体积: {total_volume}")
if __name__ == "__main__": if __name__ == "__main__":
# ---- 资源SUP 供液X中间板 R14 孔空),目标板 R24 孔空)---- with open("prcxi_material.json", "r") as f:
sup = MaterialResource("SUP", slot=5, well=[1], liquid_id="X", volume=10000) material_info = json.load(f)
r1 = MaterialResource("R1", slot=6, well=[1,2,3,4,5,6,7,8])
r2 = MaterialResource("R2", slot=7, well=[1,2,3,4,5,6,7,8]) layout = DefaultLayout("PRCXI9320")
layout.add_lab_resource(material_info)
pm = ProtocolManager() plan = layout.recommend_layout({
# 步骤1SUP -> R11->N 扇出,每孔 50 uL总 200 uL "10μL加长 Tip头": 2,
pm.add_transfer(sup, r1, unit_volume=10.0) "300μL Tip头": 2,
# 步骤2R1 -> R2N->N 对应,每对 25 uL总 100 uL来自 R1 中已存在的混合物 X "96深孔板": 2,
pm.add_transfer(r1, r2, unit_volume=120.0) })
out = pm.compute_min_initials_with_tips()
# layout_planer = DefaultLayout('PRCXI9320') # if __name__ == "__main__":
# print(layout_planer.get_layout()) # # ---- 资源SUP 供液X中间板 R14 孔空),目标板 R24 孔空)----
# print("回推最小需求:", out["liquid_setup"]) # {'SUP': {'X': 200.0}} # # sup = MaterialResource("SUP", slot=5, well=[1], liquid_id="X", volume=10000)
# print("步骤枪头建议:", out["step_tips"]) # [{'idx':0,'tip':'TIP_200uL','unit_volume':50.0}, {'idx':1,'tip':'TIP_50uL','unit_volume':25.0}] # # r1 = MaterialResource("R1", slot=6, well=[1,2,3,4,5,6,7,8])
# # r2 = MaterialResource("R2", slot=7, well=[1,2,3,4,5,6,7,8])
# # 实际执行(可选) # # pm = ProtocolManager()
# transfer_liquid(sup, r1, unit_volume=50.0) # # # 步骤1SUP -> R11->N 扇出,每孔 50 uL总 200 uL
# transfer_liquid(r1, r2, unit_volume=25.0) # # pm.add_transfer(sup, r1, unit_volume=10.0)
# print("执行后 SUP", sup.get_resource()) # 总体积 -200 # # # 步骤2R1 -> R2N->N 对应,每对 25 uL总 100 uL来自 R1 中已存在的混合物 X
# print("执行后 R1", r1.get_resource()) # 每孔 25 uL50 进 -25 出) # # pm.add_transfer(r1, r2, unit_volume=120.0)
# print("执行后 R2", r2.get_resource()) # 每孔 25 uL
# # out = pm.compute_min_initials_with_tips()
# # # layout_planer = DefaultLayout('PRCXI9320')
# # # print(layout_planer.get_layout())
# # # print("回推最小需求:", out["liquid_setup"]) # {'SUP': {'X': 200.0}}
# # # print("步骤枪头建议:", out["step_tips"]) # [{'idx':0,'tip':'TIP_200uL','unit_volume':50.0}, {'idx':1,'tip':'TIP_50uL','unit_volume':25.0}]
# # # # 实际执行(可选)
# # # transfer_liquid(sup, r1, unit_volume=50.0)
# # # transfer_liquid(r1, r2, unit_volume=25.0)
# # # print("执行后 SUP", sup.get_resource()) # 总体积 -200
# # # print("执行后 R1", r1.get_resource()) # 每孔 25 uL50 进 -25 出)
# # # print("执行后 R2", r2.get_resource()) # 每孔 25 uL
from pylabrobot.resources.opentrons.tube_racks import * # # from pylabrobot.resources.opentrons.tube_racks import *
from pylabrobot.resources.opentrons.plates import * # # from pylabrobot.resources.opentrons.plates import *
from pylabrobot.resources.opentrons.tip_racks import * # # from pylabrobot.resources.opentrons.tip_racks import *
from pylabrobot.resources.opentrons.reservoirs import * # # from pylabrobot.resources.opentrons.reservoirs import *
plate = [locals()['nest_96_wellplate_2ml_deep'](name="thermoscientificnunc_96_wellplate_2000ul"), locals()['corning_96_wellplate_360ul_flat'](name="corning_96_wellplate_360ul_flat")] # # plate = [locals()['nest_96_wellplate_2ml_deep'](name="thermoscientificnunc_96_wellplate_2000ul"), locals()['corning_96_wellplate_360ul_flat'](name="corning_96_wellplate_360ul_flat")]
tiprack = [locals()['opentrons_96_tiprack_300ul'](name="opentrons_96_tiprack_300ul"), locals()['opentrons_96_tiprack_1000ul'](name="opentrons_96_tiprack_1000ul")] # # tiprack = [locals()['opentrons_96_tiprack_300ul'](name="opentrons_96_tiprack_300ul"), locals()['opentrons_96_tiprack_1000ul'](name="opentrons_96_tiprack_1000ul")]
trash = [locals()['axygen_1_reservoir_90ml'](name="axygen_1_reservoir_90ml")] # # trash = [locals()['axygen_1_reservoir_90ml'](name="axygen_1_reservoir_90ml")]
# # from pprint import pprint
# # lab_resource = LabResource()
# # lab_resource.add_tipracks(tiprack)
# # lab_resource.add_plates(plate)
# # lab_resource.add_trash(trash)
# # layout_planer = DefaultLayout('PRCXI9300')
# # layout_planer.add_lab_resource(lab_resource)
# # layout_planer.recommend_layout(out)
# with open("prcxi_material.json", "r") as f:
# material_info = json.load(f)
# # print("当前实验物料信息:", material_info)
# layout = DefaultLayout("PRCXI9320")
# layout.add_lab_resource(material_info)
# print(layout.default_layout['WorkTablets'])
# # plan = layout.recommend_layout({
# # "10μL加长 Tip头": 2,
# # "300μL Tip头": 2,
# # "96深孔板": 2,
# # })
from pprint import pprint
lab_resource = LabResource()
lab_resource.add_tipracks(tiprack)
lab_resource.add_plates(plate)
lab_resource.add_trash(trash)
layout_planer = DefaultLayout('PRCXI9300')
layout_planer.add_lab_resource(lab_resource)
layout_planer.recommend_layout(out)

View File

@@ -808,7 +808,7 @@ class PRCXI9300Api:
return self.call("IMatrix", "GetWorkTabletMatrixById", [matrix_id]) return self.call("IMatrix", "GetWorkTabletMatrixById", [matrix_id])
def add_WorkTablet_Matrix(self, matrix: MatrixInfo): def add_WorkTablet_Matrix(self, matrix: MatrixInfo):
return self.call("IMatrix", "AddWorkTabletMatrix", [matrix]) return self.call("IMatrix", "AddWorkTabletMatrix2", [matrix])
def Load(self, dosage: int, plate_no: int, is_whole_plate: bool, hole_row: int, hole_col: int, blending_times: int, def Load(self, dosage: int, plate_no: int, is_whole_plate: bool, hole_row: int, hole_col: int, blending_times: int,
balance_height: int, plate_or_hole: str, hole_numbers: str, assist_fun1: str = "", assist_fun2: str = "", balance_height: int, plate_or_hole: str, hole_numbers: str, assist_fun1: str = "", assist_fun2: str = "",
@@ -971,6 +971,102 @@ class PRCXI9300Api:
"HoleNumbers": hole_numbers, "HoleNumbers": hole_numbers,
"LiquidDispensingMethod": liquid_method, "LiquidDispensingMethod": liquid_method,
} }
class DefaultLayout:
def __init__(self, product_name: str = "PRCXI9300"):
self.labresource = {}
if product_name not in ["PRCXI9300", "PRCXI9320"]:
raise ValueError(f"Unsupported product_name: {product_name}. Only 'PRCXI9300' and 'PRCXI9320' are supported.")
if product_name == "PRCXI9300":
self.rows = 2
self.columns = 3
self.layout = [1, 2, 3, 4, 5, 6]
self.trash_slot = 3
self.waste_liquid_slot = 6
elif product_name == "PRCXI9320":
self.rows = 3
self.columns = 4
self.layout = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
self.trash_slot = 16
self.waste_liquid_slot = 12
self.default_layout = {"MatrixId":f"{time.time()}","MatrixName":f"{time.time()}","MatrixCount":16,"WorkTablets":
[{"Number": 1, "Code": "T1", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 2, "Code": "T2", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 3, "Code": "T3", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 4, "Code": "T4", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 5, "Code": "T5", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 6, "Code": "T6", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 7, "Code": "T7", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 8, "Code": "T8", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 9, "Code": "T9", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 10, "Code": "T10", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 11, "Code": "T11", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 12, "Code": "T12", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}}, # 这个设置成废液槽,用储液槽表示
{"Number": 13, "Code": "T13", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 14, "Code": "T14", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 15, "Code": "T15", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{"Number": 16, "Code": "T16", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}} # 这个设置成垃圾桶,用储液槽表示
]
}
def get_layout(self) -> Dict[str, Any]:
return {
"rows": self.rows,
"columns": self.columns,
"layout": self.layout,
"trash_slot": self.trash_slot,
"waste_liquid_slot": self.waste_liquid_slot
}
def get_trash_slot(self) -> int:
return self.trash_slot
def get_waste_liquid_slot(self) -> int:
return self.waste_liquid_slot
def add_lab_resource(self, material_info):
self.labresource = material_info
def recommend_layout(self, needs: Dict[str, int]) -> Dict[str, Any]:
"""根据 needs 推荐布局"""
for k, v in needs.items():
if k not in self.labresource:
raise ValueError(f"Material {k} not found in lab resources.")
# 预留位置12和16不动
reserved_positions = {12, 16}
available_positions = [i for i in range(1, 17) if i not in reserved_positions]
# 计算总需求
total_needed = sum(needs.values())
if total_needed > len(available_positions):
raise ValueError(f"需要 {total_needed} 个位置,但只有 {len(available_positions)} 个可用位置排除位置12和16")
# 依次分配位置
current_pos = 0
for material_name, count in needs.items():
material_uuid = self.labresource[material_name]['uuid']
material_enum = self.labresource[material_name]['materialEnum']
for _ in range(count):
if current_pos >= len(available_positions):
raise ValueError("位置不足,无法分配更多物料")
position = available_positions[current_pos]
# 找到对应的tablet并更新
for tablet in self.default_layout['WorkTablets']:
if tablet['Number'] == position:
tablet['Material']['uuid'] = material_uuid
tablet['Material']['materialEnum'] = material_enum
break
current_pos += 1
return self.default_layout
if __name__ == "__main__": if __name__ == "__main__":
@@ -1299,10 +1395,13 @@ if __name__ == "__main__":
# from pylabrobot.resources import set_tip_tracking # from pylabrobot.resources import set_tip_tracking
set_volume_tracking(enabled=True) set_volume_tracking(enabled=True)
plate_2_liquids = handler.set_group("water", [plate2.children[0]], [300])
#print(plate_2_liquids) # 第一种情景:一个孔往多个孔加液
# plate_2_liquids = handler.set_group("water", [plate2.children[0]], [300])
# plate5_liquids = handler.set_group("master_mix", plate5.children[:23], [100]*23)
# 第二个情景:多个孔往多个孔加液(但是个数得对应)
plate_2_liquids = handler.set_group("water", plate2.children[:23], [300]*23)
plate5_liquids = handler.set_group("master_mix", plate5.children[:23], [100]*23) plate5_liquids = handler.set_group("master_mix", plate5.children[:23], [100]*23)
#print(plate5_liquids)
# plate11.set_well_liquids([("Water", 100) if (i % 8 == 0 and i // 8 < 6) else (None, 100) for i in range(96)]) # Set liquids for every 8 wells in plate8 # plate11.set_well_liquids([("Water", 100) if (i % 8 == 0 and i // 8 < 6) else (None, 100) for i in range(96)]) # Set liquids for every 8 wells in plate8
@@ -1314,7 +1413,6 @@ if __name__ == "__main__":
# print(plate11.get_well(0).tracker.get_used_volume()) # print(plate11.get_well(0).tracker.get_used_volume())
asyncio.run(handler.create_protocol(protocol_name="Test Protocol")) # Initialize the backend and setup the connection asyncio.run(handler.create_protocol(protocol_name="Test Protocol")) # Initialize the backend and setup the connection
asyncio.run(handler.transfer_group("water", "master_mix", 10)) # Reset tip tracking asyncio.run(handler.transfer_group("water", "master_mix", 10)) # Reset tip tracking
@@ -1326,7 +1424,7 @@ if __name__ == "__main__":
# # asyncio.run(handler.run_protocol()) # # asyncio.run(handler.run_protocol())
# asyncio.run(handler.dispense([plate1.children[0]],[10],[0])) # asyncio.run(handler.dispense([plate1.children[0]],[10],[0]))
# print(plate1.children[0]) # print(plate1.children[0])
# # asyncio.run(handler.run_protocol()) # asyncio.run(handler.run_protocol())
# asyncio.run(handler.mix([plate1.children[0]], mix_time=3, mix_vol=5, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100)) # asyncio.run(handler.mix([plate1.children[0]], mix_time=3, mix_vol=5, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
# print(plate1.children[0]) # print(plate1.children[0])
# asyncio.run(handler.discard_tips([0])) # asyncio.run(handler.discard_tips([0]))
@@ -1421,3 +1519,28 @@ if __name__ == "__main__":
# # # input("Press Enter to continue...") # Wait for user input before proceeding # # # input("Press Enter to continue...") # Wait for user input before proceeding
# # # print("PRCXI9300Handler initialized with deck and host settings.") # # # print("PRCXI9300Handler initialized with deck and host settings.")
# 一些推荐版位组合的测试样例:
with open("prcxi_material.json", "r") as f:
material_info = json.load(f)
layout = DefaultLayout("PRCXI9320")
layout.add_lab_resource(material_info)
MatrixLayout_1 = layout.recommend_layout({
"96 细胞培养皿": 3,
"12道储液槽": 1,
"200μL Tip头": 1,
"10μL加长 Tip头": 1,
})
print(MatrixLayout_1)
MatrixLayout_2 = layout.recommend_layout({
"96深孔板": 4,
"12道储液槽": 1,
"200μL Tip头": 1,
"10μL加长 Tip头": 1,
})

View File

@@ -0,0 +1,31 @@
{
"Tip头适配器 1250uL": {"uuid": "3b6f33ffbf734014bcc20e3c63e124d4", "materialEnum": "0"},
"ZHONGXI 适配器 300uL": {"uuid": "7c822592b360451fb59690e49ac6b181", "materialEnum": "0"},
"吸头10ul 适配器": {"uuid": "8cc3dce884ac41c09f4570d0bcbfb01c", "materialEnum": "0"},
"1250μL Tip头": {"uuid": "7960f49ddfe9448abadda89bd1556936", "materialEnum": "0"},
"10μL Tip头": {"uuid": "45f2ed3ad925484d96463d675a0ebf66", "materialEnum": "0"},
"10μL加长 Tip头": {"uuid": "068b3815e36b4a72a59bae017011b29f", "materialEnum": "1"},
"1000μL Tip头": {"uuid": "80652665f6a54402b2408d50b40398df", "materialEnum": "1"},
"300μL Tip头": {"uuid": "076250742950465b9d6ea29a225dfb00", "materialEnum": "1"},
"200μL Tip头": {"uuid": "7a73bb9e5c264515a8fcbe88aed0e6f7", "materialEnum": "0"},
"0.2ml PCR板": {"uuid": "73bb9b10bc394978b70e027bf45ce2d3", "materialEnum": "0"},
"2.2ml 深孔板": {"uuid": "ca877b8b114a4310b429d1de4aae96ee", "materialEnum": "0"},
"储液槽": {"uuid": "04211a2dc93547fe9bf6121eac533650", "materialEnum": "0"},
"全裙边 PCR适配器": {"uuid": "4a043a07c65a4f9bb97745e1f129b165", "materialEnum": "3"},
"储液槽 适配器": {"uuid": "6bdfdd7069df453896b0806df50f2f4d", "materialEnum": "0"},
"300ul深孔板适配器": {"uuid": "9a439bed8f3344549643d6b3bc5a5eb4", "materialEnum": "0"},
"10ul专用深孔板适配器": {"uuid": "4dc8d6ecfd0449549683b8ef815a861b", "materialEnum": "0"},
"爱津": {"uuid": "b01627718d3341aba649baa81c2c083c", "materialEnum": "0"},
"适配器": {"uuid": "adfabfffa8f24af5abfbba67b8d0f973", "materialEnum": "0"},
"废弃槽": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": "0"},
"96深孔板": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": "0"},
"384板": {"uuid": "853dcfb6226f476e8b23c250217dc7da", "materialEnum": "0"},
"4道储液槽": {"uuid": "01953864f6f140ccaa8ddffd4f3e46f5", "materialEnum": "0"},
"48孔深孔板": {"uuid": "026c5d5cf3d94e56b4e16b7fb53a995b", "materialEnum": "2"},
"12道储液槽": {"uuid": "0f1639987b154e1fac78f4fb29a1f7c1", "materialEnum": "0"},
"HPLC料盘": {"uuid": "548bbc3df0d4447586f2c19d2c0c0c55", "materialEnum": "0"},
"ep适配器": {"uuid": "e146697c395e4eabb3d6b74f0dd6aaf7", "materialEnum": "0"},
"30mm适配器": {"uuid": "a0757a90d8e44e81a68f306a608694f2", "materialEnum": "0"},
"细菌培养皿": {"uuid": "b05b3b2aafd94ec38ea0cd3215ecea8f", "materialEnum": "4"},
"96 细胞培养皿":{ "uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": "0"}
}