mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-05-25 13:10:02 +00:00
Compare commits
185 Commits
workstatio
...
prcix9320
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5be601177e | ||
|
|
ad05e8c73e | ||
|
|
940abc3664 | ||
|
|
6288e37464 | ||
|
|
3aed75bc8b | ||
|
|
acb2dc9359 | ||
|
|
f22c3f4c42 | ||
|
|
7df67ea9f3 | ||
|
|
4d3a41ed0d | ||
|
|
56d25b88bd | ||
|
|
95f3e0b291 | ||
|
|
9b706236f6 | ||
|
|
9f60e65b6d | ||
|
|
59aa991988 | ||
|
|
aff340de84 | ||
|
|
2fd4270831 | ||
|
|
0d41d83ce5 | ||
|
|
9a6f744afd | ||
|
|
8164d990cc | ||
|
|
5c9c8a4ee9 | ||
|
|
68ef739f4a | ||
|
|
29a484f16f | ||
|
|
a48985720c | ||
|
|
14cf4ddc0d | ||
|
|
ad66fc1841 | ||
|
|
6b3f9756a0 | ||
|
|
afddc6e40c | ||
|
|
edd67e4880 | ||
|
|
d13d3f7dfe | ||
|
|
1ab1ed69d4 | ||
|
|
ad2e5a1c04 | ||
|
|
71d35d31af | ||
|
|
7f4b57f589 | ||
|
|
0c667e68e6 | ||
|
|
9430be51a4 | ||
|
|
a187a57430 | ||
|
|
68029217de | ||
|
|
792504e08c | ||
|
|
04c0564366 | ||
|
|
9d65718f37 | ||
|
|
35bcf6765d | ||
|
|
cdbca70222 | ||
|
|
1a267729e4 | ||
|
|
b11f6eac55 | ||
|
|
d85ff540c4 | ||
|
|
5f45a0b81b | ||
|
|
6bf9a319c7 | ||
|
|
74f0d5ee65 | ||
|
|
2596d48a2f | ||
|
|
2ac1a3242a | ||
|
|
5d208c832b | ||
|
|
786498904d | ||
|
|
a9ea9f425d | ||
|
|
b3bc951cae | ||
|
|
01df4f1115 | ||
|
|
ca985f92ab | ||
|
|
41be9e4e19 | ||
|
|
e1074f06d2 | ||
|
|
0dc273f366 | ||
|
|
2e5fac26b3 | ||
|
|
5c2da9b793 | ||
|
|
45efbfcd12 | ||
|
|
8da6fdfd0b | ||
|
|
29ea9909a5 | ||
|
|
f38f3dfc89 | ||
|
|
ee6307a568 | ||
|
|
8a0116c852 | ||
|
|
d3f59913b0 | ||
|
|
f6d46e669d | ||
|
|
abf5555e37 | ||
|
|
e4d915c59c | ||
|
|
11a38d4558 | ||
|
|
aeeb36d075 | ||
|
|
3478bfd7ed | ||
|
|
d6910da57d | ||
|
|
d5b4f07406 | ||
|
|
470d7283e4 | ||
|
|
03f7f44c77 | ||
|
|
6f600b4fc7 | ||
|
|
269ce440d1 | ||
|
|
be054589b5 | ||
|
|
b045ab4e0a | ||
|
|
4595f86725 | ||
|
|
44a4c2362d | ||
|
|
1340bae838 | ||
|
|
ae75f07c8e | ||
|
|
18d0ba7a46 | ||
|
|
de7fbe7ac8 | ||
|
|
31e8d065c4 | ||
|
|
219a480c08 | ||
|
|
e9f1a7bb44 | ||
|
|
ead43b2bc1 | ||
|
|
cef86fd98d | ||
|
|
6993e97ae9 | ||
|
|
db396bcab3 | ||
|
|
1fed8de57d | ||
|
|
63eb0c0a4c | ||
|
|
888c6cf542 | ||
|
|
cc248fc32c | ||
|
|
cfe64b023b | ||
|
|
ad1312cf26 | ||
|
|
799813f85b | ||
|
|
19c9d655d0 | ||
|
|
f9a9e35269 | ||
|
|
8cd306cd32 | ||
|
|
816a0d747b | ||
|
|
b0cff1a7a8 | ||
|
|
71d57c5631 | ||
|
|
546fb633ec | ||
|
|
a3c7fa9385 | ||
|
|
c6cf84def0 | ||
|
|
86512a0482 | ||
|
|
3ddbc1c9b7 | ||
|
|
abf1005241 | ||
|
|
c475eabb60 | ||
|
|
3ad20c85a5 | ||
|
|
44fc80c70f | ||
|
|
8ba911bb55 | ||
|
|
896f287d92 | ||
|
|
0d150f7acd | ||
|
|
c27f7e42d6 | ||
|
|
cc56a68bc6 | ||
|
|
d7302c3b35 | ||
|
|
b46a51c40e | ||
|
|
c6780087b8 | ||
|
|
1ef698dde6 | ||
|
|
91aadba4ef | ||
|
|
b1cdef9185 | ||
|
|
9854ed8c9c | ||
|
|
52544a2c69 | ||
|
|
5ce433e235 | ||
|
|
c7c14d2332 | ||
|
|
6fdd482649 | ||
|
|
d390236318 | ||
|
|
ed8ee29732 | ||
|
|
ffc583e9d5 | ||
|
|
f1ad0c9c96 | ||
|
|
8fa3407649 | ||
|
|
d3282822fc | ||
|
|
554bcade24 | ||
|
|
a662c75de1 | ||
|
|
931614fe64 | ||
|
|
d39662f65f | ||
|
|
acf5fdebf8 | ||
|
|
7f7b1c13c0 | ||
|
|
75f09034ff | ||
|
|
549a50220b | ||
|
|
4189a2cfbe | ||
|
|
48895a9bb1 | ||
|
|
891f126ed6 | ||
|
|
4d3475a849 | ||
|
|
b475db66df | ||
|
|
a625a86e3e | ||
|
|
37e0f1037c | ||
|
|
a242253145 | ||
|
|
448e0074b7 | ||
|
|
304827fc8d | ||
|
|
872b3d781f | ||
|
|
813400f2b4 | ||
|
|
b6dfe2b944 | ||
|
|
8807865649 | ||
|
|
5fc7eb7586 | ||
|
|
9bd72b48e1 | ||
|
|
42b78ab4c1 | ||
|
|
9645609a05 | ||
|
|
a2a827d7ac | ||
|
|
bb3ca645a4 | ||
|
|
37ee43d19a | ||
|
|
bc30f23e34 | ||
|
|
166d84afe1 | ||
|
|
1b43c53015 | ||
|
|
d4415f5a35 | ||
|
|
0260cbbedb | ||
|
|
7c440d10ab | ||
|
|
c85c49817d | ||
|
|
c70eafa5f0 | ||
|
|
b64466d443 | ||
|
|
ef3f24ed48 | ||
|
|
2a8e8d014b | ||
|
|
e0da1c7217 | ||
|
|
51d3e61723 | ||
|
|
6b5765bbf3 | ||
|
|
eb1f3fbe1c | ||
|
|
fb93b1cd94 | ||
|
|
9aeffebde1 |
@@ -3,7 +3,7 @@
|
||||
|
||||
package:
|
||||
name: unilabos
|
||||
version: 0.11.2
|
||||
version: 0.11.1
|
||||
|
||||
source:
|
||||
path: ../../unilabos
|
||||
@@ -54,7 +54,7 @@ requirements:
|
||||
- pymodbus
|
||||
- matplotlib
|
||||
- pylibftdi
|
||||
- uni-lab::unilabos-env ==0.11.2
|
||||
- uni-lab::unilabos-env ==0.11.1
|
||||
|
||||
about:
|
||||
repository: https://github.com/deepmodeling/Uni-Lab-OS
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
package:
|
||||
name: unilabos-env
|
||||
version: 0.11.2
|
||||
version: 0.11.1
|
||||
|
||||
build:
|
||||
noarch: generic
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
package:
|
||||
name: unilabos-full
|
||||
version: 0.11.2
|
||||
version: 0.11.1
|
||||
|
||||
build:
|
||||
noarch: generic
|
||||
@@ -11,7 +11,7 @@ build:
|
||||
requirements:
|
||||
run:
|
||||
# Base unilabos package (includes unilabos-env)
|
||||
- uni-lab::unilabos ==0.11.2
|
||||
- uni-lab::unilabos ==0.11.1
|
||||
# Documentation tools
|
||||
- sphinx
|
||||
- sphinx_rtd_theme
|
||||
|
||||
9
.conda/scripts/post-link.bat
Normal file
9
.conda/scripts/post-link.bat
Normal file
@@ -0,0 +1,9 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM upgrade pip
|
||||
"%PREFIX%\python.exe" -m pip install --upgrade pip
|
||||
|
||||
REM install extra deps
|
||||
"%PREFIX%\python.exe" -m pip install paho-mqtt opentrons_shared_data
|
||||
"%PREFIX%\python.exe" -m pip install git+https://github.com/Xuwznln/pylabrobot.git
|
||||
9
.conda/scripts/post-link.sh
Normal file
9
.conda/scripts/post-link.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euxo pipefail
|
||||
|
||||
# make sure pip is available
|
||||
"$PREFIX/bin/python" -m pip install --upgrade pip
|
||||
|
||||
# install extra deps
|
||||
"$PREFIX/bin/python" -m pip install paho-mqtt opentrons_shared_data
|
||||
"$PREFIX/bin/python" -m pip install git+https://github.com/Xuwznln/pylabrobot.git
|
||||
@@ -1,450 +0,0 @@
|
||||
---
|
||||
name: filter-workflow-by-tags
|
||||
description: Query backend workflow list, aggregate all tags, and filter workflows by domain/scenario requirements using tags. Use when the user wants to search workflows, find workflows by tags, list available workflow tags, filter workflows by category/domain/scenario, or mentions 工作流筛选/标签查询/workflow tags/按领域查找工作流.
|
||||
---
|
||||
# Uni-Lab 工作流标签筛选指南
|
||||
|
||||
通过 Uni-Lab 云端 API 查询工作流列表,汇总所有可用标签(tags),并根据领域和场景要求筛选工作流。
|
||||
|
||||
> **重要**:本指南中的 `Authorization: Lab <token>` 是 **Uni-Lab 平台专用的认证方式**,`Lab` 是 Uni-Lab 的 auth scheme 关键字,**不是** HTTP Basic 认证。请勿将其替换为 `Basic`。
|
||||
|
||||
## 使用模式识别
|
||||
|
||||
**用户可能一开始就给出场景目标**(如"我要做有机合成实验"、"找柱层析相关的 protocol")。此时:
|
||||
|
||||
1. **识别场景关键词** → 映射到可能的 tags(如 synthesis、organic、chromatography、purification)
|
||||
2. **直接执行完整流程**(获取 ak/sk/addr → 拉取所有工作流 → 汇总 tags → 按场景筛选)
|
||||
3. **展示筛选结果** → 引导用户从候选 workflow 中**选择明确的实验 protocol**
|
||||
4. **如果用户确认某个 workflow** → 记录 `workflow_uuid`,准备对接“与其他 Skill 的协作”
|
||||
|
||||
**如果用户未给场景目标**,则按标准 checklist 询问筛选条件。
|
||||
|
||||
---
|
||||
|
||||
## 前置条件
|
||||
|
||||
使用本指南前,**必须**先确认以下信息。如果缺少任何一项,**立即向用户询问并终止**,等补齐后再继续。
|
||||
|
||||
### 1. ak / sk → AUTH
|
||||
|
||||
询问用户的启动参数,从 `--ak` `--sk` 或 config.py 中获取。
|
||||
|
||||
生成 AUTH token:
|
||||
|
||||
```bash
|
||||
python -c "import base64,sys; print('Authorization: Lab ' + base64.b64encode(f'{sys.argv[1]}:{sys.argv[2]}'.encode()).decode())" <ak> <sk>
|
||||
```
|
||||
|
||||
### 2. --addr → BASE URL
|
||||
|
||||
| `--addr` 值 | BASE |
|
||||
| ------------- | ------------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
|
||||
确认后设置:
|
||||
|
||||
```bash
|
||||
BASE="<根据 addr 确定的 URL>"
|
||||
AUTH="Authorization: Lab <上面命令输出的 token>"
|
||||
```
|
||||
|
||||
### 3. lab_uuid(实验室 UUID)
|
||||
|
||||
如果用户未提供 `lab_uuid`,通过以下 API 自动获取:
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 `data.uuid` 即为 `lab_uuid`。
|
||||
|
||||
**三项全部就绪后才可开始。**
|
||||
|
||||
## Session State
|
||||
|
||||
在整个对话过程中,agent 需要记住以下状态:
|
||||
|
||||
- `lab_uuid` — 实验室 UUID
|
||||
- `all_workflows` — 完整工作流列表(分页获取后缓存到内存或临时文件)
|
||||
- `all_tags` — 所有工作流的标签汇总
|
||||
|
||||
---
|
||||
|
||||
## API 端点
|
||||
|
||||
### 查询工作流列表(支持分页)
|
||||
|
||||
```
|
||||
GET $BASE/api/v1/lab/workflow/owner/list?page=<page>&page_size=<page_size>&lab_uuid=$lab_uuid
|
||||
```
|
||||
|
||||
**参数:**
|
||||
|
||||
- `page` — 页码,从 1 开始
|
||||
- `page_size` — 每页数量,建议 1000
|
||||
- `lab_uuid` — 实验室 UUID
|
||||
|
||||
**返回结构:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"has_more": true,
|
||||
"data": [
|
||||
{
|
||||
"uuid": "9661bba2-1b9f-4687-a63d-910245df174b",
|
||||
"name": "Untitled",
|
||||
"description": "",
|
||||
"user_id": "114211",
|
||||
"published": false,
|
||||
"tags": null
|
||||
},
|
||||
{
|
||||
"uuid": "e0436638-190b-46bc-b1a1-2711d9602f6a",
|
||||
"name": "Synthesis v2",
|
||||
"user_id": "114211",
|
||||
"published": true,
|
||||
"tags": ["synthesis", "organic"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
|
||||
- `has_more` — 若为 `true`,需要继续请求 `page+1`
|
||||
- `tags` — 可能为 `null`、空数组或字符串数组;聚合时必须容忍 `null`
|
||||
|
||||
### 启动工作流(直接运行)
|
||||
|
||||
```
|
||||
POST $BASE/api/v1/lab/workflow/<workflow_uuid>/run
|
||||
```
|
||||
|
||||
**用途:** 直接启动一个 workflow 的默认执行(使用模板中预设的参数),无需创建 notebook。适用于快速测试或无参数变化的重复执行。
|
||||
|
||||
**请求体:** 空 JSON `{}` 或省略
|
||||
|
||||
**返回:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": "<run_uuid>"
|
||||
}
|
||||
```
|
||||
|
||||
- `run_uuid` — 本次执行的唯一标识(不是 notebook UUID)
|
||||
|
||||
**注意:**
|
||||
|
||||
- 该接口会使用 workflow 模板中保存的默认参数直接执行
|
||||
- 如果 workflow 需要动态参数(如 CSV 路径、样品 UUID),应使用 `POST /lab/notebook` 创建 notebook 并传入 `node_params`
|
||||
- 返回的 `run_uuid` 可直接传入下方「查询任务状态」接口查询实时进度
|
||||
|
||||
### 查询任务状态
|
||||
|
||||
```
|
||||
GET $BASE/api/v1/lab/mcp/task/<task_uuid>
|
||||
```
|
||||
|
||||
**用途:** 查询由 `POST /lab/workflow/<uuid>/run` 返回的 `run_uuid`(即 task_uuid)的实时执行状态,包括整体状态和每个节点(JOS:Job On Station)的执行详情。
|
||||
|
||||
**路径参数:**
|
||||
|
||||
- `task_uuid` — 等同于启动工作流接口返回的 `run_uuid`
|
||||
|
||||
**返回:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"status": "running",
|
||||
"jos_status": [
|
||||
{
|
||||
"uuid": "d0e24bfe-8d99-450e-b19d-f25849dfbaad",
|
||||
"node_name": "PRCXI_BioER_96_wellplate_slot_1",
|
||||
"action_name": "create_resource",
|
||||
"status": "success",
|
||||
"return_info": {
|
||||
"suc": true,
|
||||
"error": "",
|
||||
"return_value": { ... }
|
||||
}
|
||||
},
|
||||
{
|
||||
"uuid": "...",
|
||||
"node_name": "...",
|
||||
"action_name": "transfer_liquid",
|
||||
"status": "pending",
|
||||
"return_info": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
|
||||
- `data.status` — 整体任务状态
|
||||
- `running` — 正在执行(至少一个节点 pending 或 running)
|
||||
- `success` — 全部节点成功
|
||||
- `failed` — 有节点失败
|
||||
- `data.jos_status[]` — 节点级执行列表(按执行顺序)
|
||||
- `uuid` — 节点执行实例 UUID
|
||||
- `node_name` — 节点名称(资源/设备名或工位名)
|
||||
- `action_name` — 动作类型(`create_resource`、`transfer_liquid`、`centrifuge`、等)
|
||||
- `status` — 节点状态:`success`、`pending`、`running`、`failed`
|
||||
- `return_info` — 执行返回,失败时 `suc=false` 且 `error` 有错误信息
|
||||
|
||||
**注意:**
|
||||
|
||||
- 此接口的 `task_uuid` **是** `POST /lab/workflow/<uuid>/run` 返回的 `run_uuid`,二者为同一个 ID 的不同称呼
|
||||
- **不要**把 notebook UUID(`POST /lab/notebook` 返回)传进来——那条路径用 `GET /lab/notebook/status` 查询
|
||||
- `jos_status` 数组按节点执行顺序给出;从 pending 数量可以估算剩余进度
|
||||
- 返回体可能较大(`return_info.return_value` 中可能包含完整 resource tree),可在脚本中只提取 `status` + `node_name` + `action_name` 做摘要
|
||||
|
||||
**状态轮询示例:**
|
||||
|
||||
```bash
|
||||
# 每 5 秒轮询一次直至完成
|
||||
TASK="b183d97e-d2b5-4b24-b14b-820df04d87c0"
|
||||
while :; do
|
||||
st=$(curl -s -X GET "$BASE/api/v1/lab/mcp/task/$TASK" -H "$AUTH" \
|
||||
| python3 -c "import json,sys; d=json.load(sys.stdin)['data']; \
|
||||
print(d['status'], '|', sum(1 for j in d['jos_status'] if j['status']=='success'), '/', len(d['jos_status']))")
|
||||
echo "$(date +%H:%M:%S) $st"
|
||||
[[ "$st" == success* || "$st" == failed* ]] && break
|
||||
sleep 5
|
||||
done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整工作流 Checklist
|
||||
|
||||
```
|
||||
Task Progress:
|
||||
- [ ] Step 0: 识别用户是否已给出场景目标(如"有机合成"、"柱层析")
|
||||
- 若已给出 → 记录场景关键词,自动进入后续步骤
|
||||
- 若未给出 → 在 Step 6 询问用户
|
||||
- [ ] Step 1: 确认 ak/sk → 生成 AUTH token
|
||||
- [ ] Step 2: 确认 --addr → 设置 BASE URL
|
||||
- [ ] Step 3: GET /edge/lab/info → 获取 lab_uuid(如用户未提供)
|
||||
- [ ] Step 4: 分页获取所有工作流(从 page=1 开始直到 has_more=false)
|
||||
- [ ] Step 5: 汇总所有非空 tags → 生成 all_tags(去重、排序、附出现次数)
|
||||
- [ ] Step 6: 根据场景关键词(Step 0 或新询问)在 all_tags 中做语义映射 → 确定候选 tags
|
||||
- 若语义映射不唯一,列出候选 tags 让用户确认
|
||||
- [ ] Step 7: 按候选 tags 筛选工作流(默认 any 模式,召回优先)
|
||||
- [ ] Step 8: 展示筛选结果(uuid、name、description、tags、published)
|
||||
- [ ] Step 9: 引导用户从结果中选择**明确的实验 protocol**
|
||||
- 若结果只有 1 条 → 直接确认该 workflow_uuid
|
||||
- 若结果 2–10 条 → 让用户按编号选择
|
||||
- 若结果过多 → 提示收紧条件(加 tag、切换 all 模式、仅 published)
|
||||
- 若结果为空 → 放宽条件(去掉最稀有 tag)或提示用户换关键词
|
||||
- [ ] Step 10: 记录用户选中的 workflow_uuid,并提示提交实验或查看详情
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 推荐路径:使用脚本
|
||||
|
||||
同目录下提供 `scripts/filter_workflows.py`,一次完成分页抓取、标签聚合与筛选:
|
||||
|
||||
```bash
|
||||
# 1. 仅汇总标签(不筛选)
|
||||
python scripts/filter_workflows.py \
|
||||
--auth "<Lab base64token>" \
|
||||
--base "$BASE" \
|
||||
--lab-uuid "$lab_uuid" \
|
||||
--summary-only
|
||||
|
||||
# 2. 按标签筛选(ANY 模式:包含任一)
|
||||
python scripts/filter_workflows.py \
|
||||
--auth "<Lab base64token>" \
|
||||
--base "$BASE" \
|
||||
--lab-uuid "$lab_uuid" \
|
||||
--tags synthesis organic \
|
||||
--mode any
|
||||
|
||||
# 3. 按标签筛选(ALL 模式:必须同时包含)
|
||||
python scripts/filter_workflows.py \
|
||||
--auth "<Lab base64token>" \
|
||||
--base "$BASE" \
|
||||
--lab-uuid "$lab_uuid" \
|
||||
--tags synthesis organic \
|
||||
--mode all \
|
||||
--output filtered.json
|
||||
|
||||
# 4. 仅筛选已发布
|
||||
python scripts/filter_workflows.py \
|
||||
--auth "<Lab base64token>" \
|
||||
--base "$BASE" \
|
||||
--lab-uuid "$lab_uuid" \
|
||||
--tags synthesis \
|
||||
--published-only
|
||||
```
|
||||
|
||||
**`--auth` 参数说明**:传入 `Authorization` 头中 `Lab` 之后的 base64 token(不带 `Lab ` 前缀),脚本内部会自动补上 scheme。
|
||||
|
||||
**输出结构:**
|
||||
|
||||
```json
|
||||
{
|
||||
"total_workflows": 150,
|
||||
"tag_counts": {"synthesis": 12, "organic": 8, "analysis": 5},
|
||||
"all_tags": ["analysis", "organic", "synthesis"],
|
||||
"filter": {"tags": ["synthesis", "organic"], "mode": "any"},
|
||||
"filtered_workflows": [
|
||||
{"uuid": "...", "name": "...", "description": "...", "tags": [...], "published": true}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 手动路径:curl + jq
|
||||
|
||||
如果脚本不可用或环境缺少 Python,可用 shell 实现。
|
||||
|
||||
### 1. 分页抓取(写入 `all_workflows.json`)
|
||||
|
||||
```bash
|
||||
page=1
|
||||
echo "[]" > all_workflows.json
|
||||
|
||||
while :; do
|
||||
resp=$(curl -s -X GET \
|
||||
"$BASE/api/v1/lab/workflow/owner/list?page=$page&page_size=1000&lab_uuid=$lab_uuid" \
|
||||
-H "$AUTH")
|
||||
|
||||
page_data=$(echo "$resp" | jq -c '.data.data // []')
|
||||
jq -c --argjson p "$page_data" '. + $p' all_workflows.json > _tmp.json && mv _tmp.json all_workflows.json
|
||||
|
||||
has_more=$(echo "$resp" | jq -r '.data.has_more')
|
||||
[ "$has_more" != "true" ] && break
|
||||
page=$((page + 1))
|
||||
done
|
||||
|
||||
echo "Total: $(jq 'length' all_workflows.json)"
|
||||
```
|
||||
|
||||
### 2. 汇总所有标签(含出现次数)
|
||||
|
||||
```bash
|
||||
jq '[.[].tags // [] | .[]] | group_by(.) | map({tag: .[0], count: length}) | sort_by(-.count)' \
|
||||
all_workflows.json
|
||||
```
|
||||
|
||||
### 3. 按标签筛选
|
||||
|
||||
```bash
|
||||
# ANY:包含任一指定标签
|
||||
jq --argjson want '["synthesis","organic"]' \
|
||||
'[.[] | select((.tags // []) | any(. as $t | $want | index($t)))]' \
|
||||
all_workflows.json
|
||||
|
||||
# ALL:同时包含所有指定标签
|
||||
jq --argjson want '["synthesis","organic"]' \
|
||||
'[.[] | select(($want | all(. as $w | (.tags // []) | index($w))))]' \
|
||||
all_workflows.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 筛选策略
|
||||
|
||||
agent 拿到用户的「领域 + 场景」自然语言描述时,按如下顺序选择 tag:
|
||||
|
||||
1. **优先用户显式指定的 tags**:若用户明确给出标签词,直接精确匹配。
|
||||
2. **从 all_tags 中做语义映射**:若用户描述是自然语言(如"有机合成、柱层析"),在 all_tags 中找语义相关项(如 `synthesis`、`organic`、`chromatography`)。必要时展示候选 tag 让用户确认。
|
||||
3. **模式选择**:
|
||||
- 默认 `any`(更多召回),给出 tag 集合的并集匹配
|
||||
- 用户强调"必须同时满足"时用 `all`
|
||||
4. **空结果兜底**:如果筛选为空,放宽条件(去掉最稀有 tag、切换 any 模式),并提醒用户。
|
||||
|
||||
---
|
||||
|
||||
## 引导到明确的 Protocol
|
||||
|
||||
筛选完成后,**最终目标是让用户确认一个具体的 workflow_uuid**,而不是停留在"一堆候选"上。按结果数量采取不同策略:
|
||||
|
||||
| 结果数量 | 策略 |
|
||||
| --------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 0 条 | 放宽筛选(去掉最稀有 tag → 切换 any 模式 → 去掉 `--published-only`)。仍为空则提示换关键词,或列出 `all_tags` 让用户重新选。 |
|
||||
| 1 条 | 直接确认:"找到唯一匹配:`<name>` (uuid `<uuid>`),是否用它?"用户确认后记录 `workflow_uuid`。 |
|
||||
| 2–10 条 | 编号列表展示,让用户选编号。每项给出 name、tags、description 摘要、published 状态。 |
|
||||
| 10–30 条 | 先展示 tag 分布帮助用户进一步收紧:列出匹配结果中最常见的子标签,提示"加一个 tag 可将结果缩小到 N 条"。 |
|
||||
| >30 条 | 强制要求用户补充条件:仅 published、指定具体 tag 组合、或按名称关键词过滤。 |
|
||||
|
||||
**确认 workflow 后**:
|
||||
|
||||
1. 将 `workflow_uuid` 写入 session state
|
||||
2. 提示用户下一步可用的 skill:
|
||||
- 提交实验 → 引导到“与其他 Skill 的协作”
|
||||
- 查看 workflow 详细节点 → `GET /api/v1/lab/workflow/template/detail/<workflow_uuid>`
|
||||
3. 若用户想换一个,回到筛选步骤。
|
||||
|
||||
---
|
||||
|
||||
## 展示结果
|
||||
|
||||
推荐格式(表格 + 汇总统计):
|
||||
|
||||
```
|
||||
共 150 个工作流,其中 32 个匹配筛选条件 [tags: synthesis OR organic]
|
||||
|
||||
| UUID (短) | 名称 | Tags | 已发布 |
|
||||
|-----------|--------------------------|------------------------------|--------|
|
||||
| e0436638 | Synthesis v2 | synthesis, organic | ✓ |
|
||||
| 5b60dbb8 | Grignard Protocol | synthesis, organometallic | ✓ |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
所有可用标签(按频次):
|
||||
synthesis (12), organic (8), analysis (5), purification (4), ...
|
||||
```
|
||||
|
||||
如果用户下一步想执行某工作流 → 引导到“与其他 Skill 的协作”。
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: tags 为 null 的工作流要不要展示?
|
||||
|
||||
默认**不展示**在筛选结果中(因为无法按 tag 匹配)。但在 `--summary-only` 或无筛选条件时,这些工作流仍会计入总数,并在输出中单独列出"未打标签"计数。
|
||||
|
||||
### Q: 如何按名称/描述做模糊匹配?
|
||||
|
||||
脚本未内置,但可在 jq 中组合:
|
||||
|
||||
```bash
|
||||
jq '[.[] | select((.name + " " + (.description // "")) | test("organic"; "i"))]' all_workflows.json
|
||||
```
|
||||
|
||||
### Q: `page_size=1000` 是否会被服务端限制?
|
||||
|
||||
接口通常允许最大 1000;如果返回量少于 1000 且 `has_more=false`,说明已到末页。极端情况下若服务端返回错误,可降到 200 或 500 再试。
|
||||
|
||||
### Q: 工作流数量极大(>10k)怎么办?
|
||||
|
||||
1. 先跑 `--summary-only` 了解 tag 分布
|
||||
2. 提示用户先限定 `--published-only` 或指定 tag
|
||||
3. 考虑将 `all_workflows.json` 缓存到本地,下次直接复用
|
||||
|
||||
---
|
||||
|
||||
## 与其他 Skill 的协作
|
||||
|
||||
- 正常情况下,找到 workflow 之后可以直接用它提交实验(启动工作流的 api 端点 POST $BASE/api/v1/lab/workflow/<workflow_uuid>/run,不用别的 skill)
|
||||
- **仅当需要进行多次实验时,使用 batch-submit-experiment** — 筛选到目标工作流后,`workflow_uuid` 直接用于实验提交
|
||||
|
||||
## 脚本依赖
|
||||
|
||||
`scripts/filter_workflows.py` 仅使用 Python 标准库(`urllib`、`json`、`argparse`),无需额外安装。
|
||||
@@ -1,191 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""分页拉取 Uni-Lab 工作流列表,汇总 tags 并按 tag 筛选。
|
||||
|
||||
使用示例:
|
||||
python filter_workflows.py \
|
||||
--auth <base64token> \
|
||||
--base https://leap-lab.test.bohrium.com \
|
||||
--lab-uuid a9059772-... \
|
||||
--tags synthesis organic --mode any
|
||||
|
||||
仅依赖 Python 标准库。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from collections import Counter
|
||||
|
||||
|
||||
def fetch_all_workflows(base: str, auth_token: str, lab_uuid: str, page_size: int = 1000) -> list[dict]:
|
||||
"""分页拉取所有 owner 工作流,直到 has_more=false。"""
|
||||
workflows: list[dict] = []
|
||||
page = 1
|
||||
while True:
|
||||
query = urllib.parse.urlencode(
|
||||
{"page": page, "page_size": page_size, "lab_uuid": lab_uuid}
|
||||
)
|
||||
url = f"{base.rstrip('/')}/api/v1/lab/workflow/owner/list?{query}"
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers={
|
||||
"Authorization": f"Lab {auth_token}",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
payload = json.loads(resp.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as e:
|
||||
sys.exit(f"[ERROR] HTTP {e.code} on page {page}: {e.read().decode('utf-8', 'ignore')}")
|
||||
except urllib.error.URLError as e:
|
||||
sys.exit(f"[ERROR] URL error on page {page}: {e.reason}")
|
||||
|
||||
if payload.get("code") != 0:
|
||||
sys.exit(f"[ERROR] API returned non-zero code: {payload}")
|
||||
|
||||
data = payload.get("data") or {}
|
||||
page_items = data.get("data") or []
|
||||
workflows.extend(page_items)
|
||||
|
||||
if not data.get("has_more"):
|
||||
break
|
||||
page += 1
|
||||
# 防御性兜底,避免接口异常导致无限循环
|
||||
if page > 1000:
|
||||
print(f"[WARN] page count exceeded 1000, stopping early", file=sys.stderr)
|
||||
break
|
||||
|
||||
return workflows
|
||||
|
||||
|
||||
def aggregate_tags(workflows: list[dict]) -> tuple[list[str], dict[str, int], int]:
|
||||
"""返回 (sorted_tags, tag_counts, untagged_count)。"""
|
||||
counter: Counter[str] = Counter()
|
||||
untagged = 0
|
||||
for wf in workflows:
|
||||
tags = wf.get("tags")
|
||||
if not tags:
|
||||
untagged += 1
|
||||
continue
|
||||
for t in tags:
|
||||
if isinstance(t, str) and t.strip():
|
||||
counter[t.strip()] += 1
|
||||
return sorted(counter.keys()), dict(counter), untagged
|
||||
|
||||
|
||||
def filter_workflows(
|
||||
workflows: list[dict],
|
||||
want_tags: list[str],
|
||||
mode: str,
|
||||
published_only: bool,
|
||||
) -> list[dict]:
|
||||
"""按 tag 筛选。mode 取值 any / all。"""
|
||||
want_set = {t.strip() for t in want_tags if t.strip()}
|
||||
out: list[dict] = []
|
||||
for wf in workflows:
|
||||
if published_only and not wf.get("published"):
|
||||
continue
|
||||
if not want_set:
|
||||
out.append(wf)
|
||||
continue
|
||||
tags = wf.get("tags") or []
|
||||
tag_set = {t for t in tags if isinstance(t, str)}
|
||||
if mode == "all":
|
||||
if want_set.issubset(tag_set):
|
||||
out.append(wf)
|
||||
else: # any
|
||||
if want_set & tag_set:
|
||||
out.append(wf)
|
||||
return out
|
||||
|
||||
|
||||
def project_workflow(wf: dict) -> dict:
|
||||
"""精简输出字段。"""
|
||||
return {
|
||||
"uuid": wf.get("uuid"),
|
||||
"name": wf.get("name"),
|
||||
"description": wf.get("description", ""),
|
||||
"tags": wf.get("tags") or [],
|
||||
"published": bool(wf.get("published")),
|
||||
"user_id": wf.get("user_id"),
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description="Fetch & filter Uni-Lab workflows by tags.")
|
||||
p.add_argument("--auth", required=True, help="Base64 token (the part after `Lab `).")
|
||||
p.add_argument("--base", required=True, help="Base URL, e.g. https://leap-lab.test.bohrium.com")
|
||||
p.add_argument("--lab-uuid", required=True, help="Lab UUID.")
|
||||
p.add_argument("--tags", nargs="*", default=[], help="Tags to filter by (space separated).")
|
||||
p.add_argument(
|
||||
"--mode",
|
||||
choices=["any", "all"],
|
||||
default="any",
|
||||
help="any: workflow contains at least one tag; all: workflow contains every tag.",
|
||||
)
|
||||
p.add_argument("--published-only", action="store_true", help="Only include published workflows.")
|
||||
p.add_argument("--page-size", type=int, default=1000, help="Page size, default 1000.")
|
||||
p.add_argument(
|
||||
"--summary-only",
|
||||
action="store_true",
|
||||
help="Print tag summary without applying filter (still fetches everything).",
|
||||
)
|
||||
p.add_argument("--output", help="Write JSON result to this path. If omitted, print to stdout.")
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
workflows = fetch_all_workflows(
|
||||
base=args.base,
|
||||
auth_token=args.auth,
|
||||
lab_uuid=args.lab_uuid,
|
||||
page_size=args.page_size,
|
||||
)
|
||||
sorted_tags, tag_counts, untagged = aggregate_tags(workflows)
|
||||
|
||||
if args.summary_only:
|
||||
result = {
|
||||
"total_workflows": len(workflows),
|
||||
"untagged_count": untagged,
|
||||
"tag_counts": tag_counts,
|
||||
"all_tags": sorted_tags,
|
||||
}
|
||||
else:
|
||||
filtered = filter_workflows(
|
||||
workflows,
|
||||
want_tags=args.tags,
|
||||
mode=args.mode,
|
||||
published_only=args.published_only,
|
||||
)
|
||||
result = {
|
||||
"total_workflows": len(workflows),
|
||||
"untagged_count": untagged,
|
||||
"tag_counts": tag_counts,
|
||||
"all_tags": sorted_tags,
|
||||
"filter": {
|
||||
"tags": args.tags,
|
||||
"mode": args.mode,
|
||||
"published_only": args.published_only,
|
||||
},
|
||||
"matched_count": len(filtered),
|
||||
"filtered_workflows": [project_workflow(wf) for wf in filtered],
|
||||
}
|
||||
|
||||
payload = json.dumps(result, ensure_ascii=False, indent=2)
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
f.write(payload)
|
||||
print(f"Wrote {len(workflows)} workflows summary → {args.output}", file=sys.stderr)
|
||||
else:
|
||||
print(payload)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,251 +0,0 @@
|
||||
---
|
||||
name: host-node
|
||||
description: Operate Uni-Lab host node via REST API — create resources, test latency, test resource tree, manual confirm. Use when the user mentions host_node, creating resources, resource management, testing latency, or any host node operation.
|
||||
---
|
||||
|
||||
# Host Node API Skill
|
||||
|
||||
## 设备信息
|
||||
|
||||
- **device_id**: `host_node`
|
||||
- **Python 源码**: `unilabos/ros/nodes/presets/host_node.py`
|
||||
- **设备类**: `HostNode`
|
||||
- **动作数**: 4(`create_resource`, `test_latency`, `auto-test_resource`, `manual_confirm`)
|
||||
|
||||
## 前置条件(缺一不可)
|
||||
|
||||
使用本 skill 前,**必须**先确认以下信息。如果缺少任何一项,**立即向用户询问并终止**,等补齐后再继续。
|
||||
|
||||
### 1. ak / sk → AUTH
|
||||
|
||||
从启动参数 `--ak` `--sk` 或 config.py 中获取,生成 token:`base64(ak:sk)` → `Authorization: Lab <token>`
|
||||
|
||||
### 2. --addr → BASE URL
|
||||
|
||||
| `--addr` 值 | BASE |
|
||||
| ------------ | ----------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
|
||||
确认后设置:
|
||||
|
||||
```bash
|
||||
BASE="<根据 addr 确定的 URL>"
|
||||
AUTH="Authorization: Lab <token>"
|
||||
```
|
||||
|
||||
**两项全部就绪后才可发起 API 请求。**
|
||||
|
||||
## Session State
|
||||
|
||||
在整个对话过程中,agent 需要记住以下状态,避免重复询问用户:
|
||||
|
||||
- `lab_uuid` — 实验室 UUID(首次通过 API #1 自动获取,**不需要问用户**)
|
||||
- `device_name` — `host_node`
|
||||
|
||||
## 请求约定
|
||||
|
||||
所有请求使用 `curl -s`,POST/PATCH/DELETE 需加 `Content-Type: application/json`。
|
||||
|
||||
> **Windows 平台**必须使用 `curl.exe`(而非 PowerShell 的 `curl` 别名)。
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. 获取实验室信息(自动获取 lab_uuid)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `lab_uuid`,`data.name` 为 `lab_name`。
|
||||
|
||||
### 2. 创建工作流
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/owner" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"name":"<名称>","lab_uuid":"<lab_uuid>","description":"<描述>"}'
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `workflow_uuid`。创建成功后告知用户链接:`$BASE/laboratory/$lab_uuid/workflow/$workflow_uuid`
|
||||
|
||||
### 3. 创建节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/edge/workflow/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"workflow_uuid":"<workflow_uuid>","resource_template_name":"host_node","node_template_name":"<action_name>"}'
|
||||
```
|
||||
|
||||
- `resource_template_name` 固定为 `host_node`
|
||||
- `node_template_name` — action 名称(如 `create_resource`, `test_latency`)
|
||||
|
||||
### 4. 删除节点
|
||||
|
||||
```bash
|
||||
curl -s -X DELETE "$BASE/api/v1/lab/workflow/nodes" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuids":["<uuid1>"],"workflow_uuid":"<workflow_uuid>"}'
|
||||
```
|
||||
|
||||
### 5. 更新节点参数
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH "$BASE/api/v1/lab/workflow/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"workflow_uuid":"<wf_uuid>","uuid":"<node_uuid>","param":{...}}'
|
||||
```
|
||||
|
||||
`param` 直接使用创建节点返回的 `data.param` 结构,修改需要填入的字段值。参考 [action-index.md](action-index.md) 确定哪些字段是 Slot。
|
||||
|
||||
### 6. 查询节点 handles
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/node-handles" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuids":["<node_uuid_1>","<node_uuid_2>"]}'
|
||||
```
|
||||
|
||||
### 7. 批量创建边
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/edges" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"edges":[{"source_node_uuid":"<uuid>","target_node_uuid":"<uuid>","source_handle_uuid":"<uuid>","target_handle_uuid":"<uuid>"}]}'
|
||||
```
|
||||
|
||||
### 8. 启动工作流
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/<workflow_uuid>/run" -H "$AUTH"
|
||||
```
|
||||
|
||||
### 9. 运行设备单动作
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/mcp/run/action" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"lab_uuid":"<lab_uuid>","device_id":"host_node","action":"<action_name>","action_type":"<type>","param":{...}}'
|
||||
```
|
||||
|
||||
`param` 直接放 goal 里的属性,**不要**再包一层 `{"goal": {...}}`。
|
||||
|
||||
> **WARNING: `action_type` 必须正确,传错会导致任务永远卡住无法完成。** 从下表或 `actions/<name>.json` 的 `type` 字段获取。
|
||||
|
||||
#### action_type 速查表
|
||||
|
||||
| action | action_type |
|
||||
|--------|-------------|
|
||||
| `test_latency` | `UniLabJsonCommand` |
|
||||
| `create_resource` | `ResourceCreateFromOuterEasy` |
|
||||
| `auto-test_resource` | `UniLabJsonCommand` |
|
||||
| `manual_confirm` | `UniLabJsonCommand` |
|
||||
|
||||
### 10. 查询任务状态
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/mcp/task/<task_uuid>" -H "$AUTH"
|
||||
```
|
||||
|
||||
### 11. 运行工作流单节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/mcp/run/workflow/action" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuid":"<node_uuid>"}'
|
||||
```
|
||||
|
||||
### 12. 获取资源树(物料信息)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/download/$lab_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
注意 `lab_uuid` 在路径中。返回 `data.nodes[]` 含所有节点(设备 + 物料),每个节点含 `name`、`uuid`、`type`、`parent`。
|
||||
|
||||
### 13. 获取工作流模板详情
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/workflow/template/detail/$workflow_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
> 必须使用 `/lab/workflow/template/detail/{uuid}`,其他路径会返回 404。
|
||||
|
||||
### 14. 按名称查询物料模板
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/template/by-name?lab_uuid=$lab_uuid&name=<template_name>" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `res_template_uuid`,用于 API #15。
|
||||
|
||||
### 15. 创建物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"res_template_uuid":"<uuid>","name":"<名称>","display_name":"<显示名>","parent_uuid":"<父节点uuid>","data":{...}}'
|
||||
```
|
||||
|
||||
### 16. 更新物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X PUT "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"uuid":"<节点uuid>","display_name":"<新名称>","data":{...}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Placeholder Slot 填写规则
|
||||
|
||||
| `placeholder_keys` 值 | Slot 类型 | 填写格式 | 选取范围 |
|
||||
| --------------------- | ------------ | ----------------------------------------------------- | ---------------------- |
|
||||
| `unilabos_resources` | ResourceSlot | `{"id": "/path/name", "name": "name", "uuid": "xxx"}` | 仅物料节点(非设备) |
|
||||
| `unilabos_devices` | DeviceSlot | `"/parent/device_name"` | 仅设备节点(type=device) |
|
||||
| `unilabos_nodes` | NodeSlot | `"/parent/node_name"` | 所有节点(设备 + 物料) |
|
||||
| `unilabos_class` | ClassSlot | `"class_name"` | 注册表中已注册的资源类 |
|
||||
|
||||
### host_node 设备的 Slot 字段表
|
||||
|
||||
| Action | 字段 | Slot 类型 | 说明 |
|
||||
| ----------------- | ----------- | ------------ | ------------------------------ |
|
||||
| `create_resource` | `res_id` | ResourceSlot | 新资源路径(可填不存在的路径) |
|
||||
| `create_resource` | `device_id` | DeviceSlot | 归属设备 |
|
||||
| `create_resource` | `parent` | NodeSlot | 父节点路径 |
|
||||
| `create_resource` | `class_name`| ClassSlot | 资源类名如 `"container"` |
|
||||
| `auto-test_resource` | `resource` | ResourceSlot | 单个测试物料 |
|
||||
| `auto-test_resource` | `resources` | ResourceSlot | 测试物料数组 |
|
||||
| `auto-test_resource` | `device` | DeviceSlot | 测试设备 |
|
||||
| `auto-test_resource` | `devices` | DeviceSlot | 测试设备 |
|
||||
|
||||
---
|
||||
|
||||
## 渐进加载策略
|
||||
|
||||
1. **SKILL.md**(本文件)— API 端点 + session state 管理
|
||||
2. **[action-index.md](action-index.md)** — 按分类浏览 4 个动作的描述和核心参数
|
||||
3. **[actions/\<name\>.json](actions/)** — 仅在需要构建具体请求时,加载对应 action 的完整 JSON Schema
|
||||
|
||||
---
|
||||
|
||||
## 完整工作流 Checklist
|
||||
|
||||
```
|
||||
Task Progress:
|
||||
- [ ] Step 1: GET /edge/lab/info 获取 lab_uuid
|
||||
- [ ] Step 2: 获取资源树 (GET #12) → 记住可用物料
|
||||
- [ ] Step 3: 读 action-index.md 确定要用的 action 名
|
||||
- [ ] Step 4: 创建工作流 (POST #2) → 记住 workflow_uuid,告知用户链接
|
||||
- [ ] Step 5: 创建节点 (POST #3, resource_template_name=host_node) → 记住 node_uuid + data.param
|
||||
- [ ] Step 6: 根据 _unilabos_placeholder_info 和资源树,填写 data.param 中的 Slot 字段
|
||||
- [ ] Step 7: 更新节点参数 (PATCH #5)
|
||||
- [ ] Step 8: 查询节点 handles (POST #6) → 获取各节点的 handle_uuid
|
||||
- [ ] Step 9: 批量创建边 (POST #7) → 用 handle_uuid 连接节点
|
||||
- [ ] Step 10: 启动工作流 (POST #8) 或运行单节点 (POST #11)
|
||||
- [ ] Step 11: 查询任务状态 (GET #10) 确认完成
|
||||
```
|
||||
@@ -1,58 +0,0 @@
|
||||
# Action Index — host_node
|
||||
|
||||
4 个动作,按功能分类。每个动作的完整 JSON Schema 在 `actions/<name>.json`。
|
||||
|
||||
---
|
||||
|
||||
## 资源管理
|
||||
|
||||
### `create_resource`
|
||||
|
||||
在资源树中创建新资源(容器、物料等),支持指定位置、类型和初始液体
|
||||
|
||||
- **action_type**: `ResourceCreateFromOuterEasy`
|
||||
- **Schema**: [`actions/create_resource.json`](actions/create_resource.json)
|
||||
- **可选参数**: `res_id`, `device_id`, `class_name`, `parent`, `bind_locations`, `liquid_input_slot`, `liquid_type`, `liquid_volume`, `slot_on_deck`
|
||||
- **占位符字段**:
|
||||
- `res_id` — **ResourceSlot**(特例:目标物料可能尚不存在,直接填期望路径)
|
||||
- `device_id` — **DeviceSlot**,填路径字符串如 `"/host_node"`
|
||||
- `parent` — **NodeSlot**,填路径字符串如 `"/workstation/deck"`
|
||||
- `class_name` — **ClassSlot**,填类名如 `"container"`
|
||||
|
||||
### `auto-test_resource`
|
||||
|
||||
测试资源系统,返回当前资源树和设备列表
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/test_resource.json`](actions/test_resource.json)
|
||||
- **可选参数**: `resource`, `resources`, `device`, `devices`
|
||||
- **占位符字段**:
|
||||
- `resource` — **ResourceSlot**,单个物料节点 `{id, name, uuid}`
|
||||
- `resources` — **ResourceSlot**,物料节点数组 `[{id, name, uuid}, ...]`
|
||||
- `device` — **DeviceSlot**,设备路径字符串
|
||||
- `devices` — **DeviceSlot**,设备路径字符串
|
||||
|
||||
---
|
||||
|
||||
## 系统工具
|
||||
|
||||
### `test_latency`
|
||||
|
||||
测试设备通信延迟,返回 RTT、时间差、任务延迟等指标
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/test_latency.json`](actions/test_latency.json)
|
||||
- **参数**: 无(零参数调用)
|
||||
|
||||
---
|
||||
|
||||
## 人工确认
|
||||
|
||||
### `manual_confirm`
|
||||
|
||||
创建人工确认节点,等待用户手动确认后继续
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/manual_confirm.json`](actions/manual_confirm.json)
|
||||
- **核心参数**: `timeout_seconds`(超时时间,秒), `assignee_user_ids`(指派用户 ID 列表)
|
||||
- **占位符字段**: `assignee_user_ids` — `unilabos_manual_confirm` 类型
|
||||
@@ -1,93 +0,0 @@
|
||||
{
|
||||
"type": "ResourceCreateFromOuterEasy",
|
||||
"goal": {
|
||||
"res_id": "res_id",
|
||||
"class_name": "class_name",
|
||||
"parent": "parent",
|
||||
"device_id": "device_id",
|
||||
"bind_locations": "bind_locations",
|
||||
"liquid_input_slot": "liquid_input_slot[]",
|
||||
"liquid_type": "liquid_type[]",
|
||||
"liquid_volume": "liquid_volume[]",
|
||||
"slot_on_deck": "slot_on_deck"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"res_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"device_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"class_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"bind_locations": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "bind_locations",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"liquid_input_slot": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"liquid_type": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"liquid_volume": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"slot_on_deck": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"_unilabos_placeholder_info": {
|
||||
"res_id": "unilabos_resources",
|
||||
"device_id": "unilabos_devices",
|
||||
"parent": "unilabos_nodes",
|
||||
"class_name": "unilabos_class"
|
||||
}
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {
|
||||
"res_id": "unilabos_resources",
|
||||
"device_id": "unilabos_devices",
|
||||
"parent": "unilabos_nodes",
|
||||
"class_name": "unilabos_class"
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"timeout_seconds": "timeout_seconds",
|
||||
"assignee_user_ids": "assignee_user_ids"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timeout_seconds": {
|
||||
"type": "integer"
|
||||
},
|
||||
"assignee_user_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"timeout_seconds",
|
||||
"assignee_user_ids"
|
||||
],
|
||||
"_unilabos_placeholder_info": {
|
||||
"assignee_user_ids": "unilabos_manual_confirm"
|
||||
}
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {
|
||||
"assignee_user_ids": "unilabos_manual_confirm"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {}
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"resource": "resource",
|
||||
"resources": "resources",
|
||||
"device": "device",
|
||||
"devices": "devices"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "resource"
|
||||
},
|
||||
"resources": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "resources"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"device": {
|
||||
"type": "string",
|
||||
"description": "device reference"
|
||||
},
|
||||
"devices": {
|
||||
"type": "string",
|
||||
"description": "device reference"
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"_unilabos_placeholder_info": {
|
||||
"resource": "unilabos_resources",
|
||||
"resources": "unilabos_resources",
|
||||
"device": "unilabos_devices",
|
||||
"devices": "unilabos_devices"
|
||||
}
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {
|
||||
"resource": "unilabos_resources",
|
||||
"resources": "unilabos_resources",
|
||||
"device": "unilabos_devices",
|
||||
"devices": "unilabos_devices"
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
---
|
||||
name: virtual-workbench
|
||||
description: Operate Virtual Workbench via REST API — prepare materials, move to heating stations, start heating, move to output, transfer resources. Use when the user mentions virtual workbench, virtual_workbench, 虚拟工作台, heating stations, material processing, or workbench operations.
|
||||
---
|
||||
|
||||
# Virtual Workbench API Skill
|
||||
|
||||
## 设备信息
|
||||
|
||||
- **device_id**: `virtual_workbench`
|
||||
- **Python 源码**: `unilabos/devices/virtual/workbench.py`
|
||||
- **设备类**: `VirtualWorkbench`
|
||||
- **动作数**: 6(`auto-prepare_materials`, `auto-move_to_heating_station`, `auto-start_heating`, `auto-move_to_output`, `transfer`, `manual_confirm`)
|
||||
- **设备描述**: 模拟工作台,包含 1 个机械臂(每次操作 2s,独占锁)和 3 个加热台(每次加热 60s,可并行)
|
||||
|
||||
### 典型工作流程
|
||||
|
||||
1. `prepare_materials` — 生成 A1-A5 物料(5 个 output handle)
|
||||
2. `move_to_heating_station` — 物料并发竞争机械臂,移动到空闲加热台
|
||||
3. `start_heating` — 启动加热(3 个加热台可并行)
|
||||
4. `move_to_output` — 加热完成后移到输出位置 Cn
|
||||
|
||||
## 前置条件(缺一不可)
|
||||
|
||||
使用本 skill 前,**必须**先确认以下信息。如果缺少任何一项,**立即向用户询问并终止**,等补齐后再继续。
|
||||
|
||||
### 1. ak / sk → AUTH
|
||||
|
||||
从启动参数 `--ak` `--sk` 或 config.py 中获取,生成 token:`base64(ak:sk)` → `Authorization: Lab <token>`
|
||||
|
||||
### 2. --addr → BASE URL
|
||||
|
||||
| `--addr` 值 | BASE |
|
||||
| ------------ | ----------------------------------- |
|
||||
| `test` | `https://leap-lab.test.bohrium.com` |
|
||||
| `uat` | `https://leap-lab.uat.bohrium.com` |
|
||||
| `local` | `http://127.0.0.1:48197` |
|
||||
| 不传(默认) | `https://leap-lab.bohrium.com` |
|
||||
|
||||
确认后设置:
|
||||
|
||||
```bash
|
||||
BASE="<根据 addr 确定的 URL>"
|
||||
AUTH="Authorization: Lab <token>"
|
||||
```
|
||||
|
||||
**两项全部就绪后才可发起 API 请求。**
|
||||
|
||||
## Session State
|
||||
|
||||
- `lab_uuid` — 实验室 UUID(首次通过 API #1 自动获取,**不需要问用户**)
|
||||
- `device_name` — `virtual_workbench`
|
||||
|
||||
## 请求约定
|
||||
|
||||
所有请求使用 `curl -s`,POST/PATCH/DELETE 需加 `Content-Type: application/json`。
|
||||
|
||||
> **Windows 平台**必须使用 `curl.exe`(而非 PowerShell 的 `curl` 别名)。
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. 获取实验室信息(自动获取 lab_uuid)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `lab_uuid`,`data.name` 为 `lab_name`。
|
||||
|
||||
### 2. 创建工作流
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/owner" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"name":"<名称>","lab_uuid":"<lab_uuid>","description":"<描述>"}'
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `workflow_uuid`。创建成功后告知用户链接:`$BASE/laboratory/$lab_uuid/workflow/$workflow_uuid`
|
||||
|
||||
### 3. 创建节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/edge/workflow/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"workflow_uuid":"<workflow_uuid>","resource_template_name":"virtual_workbench","node_template_name":"<action_name>"}'
|
||||
```
|
||||
|
||||
- `resource_template_name` 固定为 `virtual_workbench`
|
||||
- `node_template_name` — action 名称(如 `auto-prepare_materials`, `auto-move_to_heating_station`)
|
||||
|
||||
### 4. 删除节点
|
||||
|
||||
```bash
|
||||
curl -s -X DELETE "$BASE/api/v1/lab/workflow/nodes" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuids":["<uuid1>"],"workflow_uuid":"<workflow_uuid>"}'
|
||||
```
|
||||
|
||||
### 5. 更新节点参数
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH "$BASE/api/v1/lab/workflow/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"workflow_uuid":"<wf_uuid>","uuid":"<node_uuid>","param":{...}}'
|
||||
```
|
||||
|
||||
参考 [action-index.md](action-index.md) 确定哪些字段是 Slot。
|
||||
|
||||
### 6. 查询节点 handles
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/node-handles" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuids":["<node_uuid_1>","<node_uuid_2>"]}'
|
||||
```
|
||||
|
||||
### 7. 批量创建边
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/edges" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"edges":[{"source_node_uuid":"<uuid>","target_node_uuid":"<uuid>","source_handle_uuid":"<uuid>","target_handle_uuid":"<uuid>"}]}'
|
||||
```
|
||||
|
||||
### 8. 启动工作流
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/workflow/<workflow_uuid>/run" -H "$AUTH"
|
||||
```
|
||||
|
||||
### 9. 运行设备单动作
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/mcp/run/action" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"lab_uuid":"<lab_uuid>","device_id":"virtual_workbench","action":"<action_name>","action_type":"<type>","param":{...}}'
|
||||
```
|
||||
|
||||
`param` 直接放 goal 里的属性,**不要**再包一层 `{"goal": {...}}`。
|
||||
|
||||
> **WARNING: `action_type` 必须正确,传错会导致任务永远卡住无法完成。** 从下表或 `actions/<name>.json` 的 `type` 字段获取。
|
||||
|
||||
#### action_type 速查表
|
||||
|
||||
| action | action_type |
|
||||
|--------|-------------|
|
||||
| `auto-prepare_materials` | `UniLabJsonCommand` |
|
||||
| `auto-move_to_heating_station` | `UniLabJsonCommand` |
|
||||
| `auto-start_heating` | `UniLabJsonCommand` |
|
||||
| `auto-move_to_output` | `UniLabJsonCommand` |
|
||||
| `transfer` | `UniLabJsonCommandAsync` |
|
||||
| `manual_confirm` | `UniLabJsonCommand` |
|
||||
|
||||
### 10. 查询任务状态
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/mcp/task/<task_uuid>" -H "$AUTH"
|
||||
```
|
||||
|
||||
### 11. 运行工作流单节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/lab/mcp/run/workflow/action" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"node_uuid":"<node_uuid>"}'
|
||||
```
|
||||
|
||||
### 12. 获取资源树(物料信息)
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/download/$lab_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
注意 `lab_uuid` 在路径中。返回 `data.nodes[]` 含所有节点(设备 + 物料),每个节点含 `name`、`uuid`、`type`、`parent`。
|
||||
|
||||
### 13. 获取工作流模板详情
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/workflow/template/detail/$workflow_uuid" -H "$AUTH"
|
||||
```
|
||||
|
||||
> 必须使用 `/lab/workflow/template/detail/{uuid}`,其他路径会返回 404。
|
||||
|
||||
### 14. 按名称查询物料模板
|
||||
|
||||
```bash
|
||||
curl -s -X GET "$BASE/api/v1/lab/material/template/by-name?lab_uuid=$lab_uuid&name=<template_name>" -H "$AUTH"
|
||||
```
|
||||
|
||||
返回 `data.uuid` 为 `res_template_uuid`,用于 API #15。
|
||||
|
||||
### 15. 创建物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"res_template_uuid":"<uuid>","name":"<名称>","display_name":"<显示名>","parent_uuid":"<父节点uuid>","data":{...}}'
|
||||
```
|
||||
|
||||
### 16. 更新物料节点
|
||||
|
||||
```bash
|
||||
curl -s -X PUT "$BASE/api/v1/edge/material/node" \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d '{"uuid":"<节点uuid>","display_name":"<新名称>","data":{...}}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Placeholder Slot 填写规则
|
||||
|
||||
| `placeholder_keys` 值 | Slot 类型 | 填写格式 | 选取范围 |
|
||||
| --------------------- | ------------ | ----------------------------------------------------- | ---------------------- |
|
||||
| `unilabos_resources` | ResourceSlot | `{"id": "/path/name", "name": "name", "uuid": "xxx"}` | 仅物料节点(非设备) |
|
||||
| `unilabos_devices` | DeviceSlot | `"/parent/device_name"` | 仅设备节点(type=device) |
|
||||
| `unilabos_nodes` | NodeSlot | `"/parent/node_name"` | 所有节点(设备 + 物料) |
|
||||
| `unilabos_class` | ClassSlot | `"class_name"` | 注册表中已注册的资源类 |
|
||||
|
||||
### virtual_workbench 设备的 Slot 字段表
|
||||
|
||||
| Action | 字段 | Slot 类型 | 说明 |
|
||||
| ----------------- | ---------------- | ------------ | -------------------- |
|
||||
| `transfer` | `resource` | ResourceSlot | 待转移物料数组 |
|
||||
| `transfer` | `target_device` | DeviceSlot | 目标设备路径 |
|
||||
| `transfer` | `mount_resource` | ResourceSlot | 目标孔位数组 |
|
||||
| `manual_confirm` | `resource` | ResourceSlot | 确认用物料数组 |
|
||||
| `manual_confirm` | `target_device` | DeviceSlot | 确认用目标设备 |
|
||||
| `manual_confirm` | `mount_resource` | ResourceSlot | 确认用目标孔位数组 |
|
||||
|
||||
> `prepare_materials`、`move_to_heating_station`、`start_heating`、`move_to_output` 这 4 个动作**无 Slot 字段**,参数为纯数值/整数。
|
||||
|
||||
---
|
||||
|
||||
## 渐进加载策略
|
||||
|
||||
1. **SKILL.md**(本文件)— API 端点 + session state 管理 + 设备工作流概览
|
||||
2. **[action-index.md](action-index.md)** — 按分类浏览 6 个动作的描述和核心参数
|
||||
3. **[actions/\<name\>.json](actions/)** — 仅在需要构建具体请求时,加载对应 action 的完整 JSON Schema
|
||||
|
||||
---
|
||||
|
||||
## 完整工作流 Checklist
|
||||
|
||||
```
|
||||
Task Progress:
|
||||
- [ ] Step 1: GET /edge/lab/info 获取 lab_uuid
|
||||
- [ ] Step 2: 获取资源树 (GET #12) → 记住可用物料
|
||||
- [ ] Step 3: 读 action-index.md 确定要用的 action 名
|
||||
- [ ] Step 4: 创建工作流 (POST #2) → 记住 workflow_uuid,告知用户链接
|
||||
- [ ] Step 5: 创建节点 (POST #3, resource_template_name=virtual_workbench) → 记住 node_uuid + data.param
|
||||
- [ ] Step 6: 根据 _unilabos_placeholder_info 和资源树,填写 data.param 中的 Slot 字段
|
||||
- [ ] Step 7: 更新节点参数 (PATCH #5)
|
||||
- [ ] Step 8: 查询节点 handles (POST #6) → 获取各节点的 handle_uuid
|
||||
- [ ] Step 9: 批量创建边 (POST #7) → 用 handle_uuid 连接节点
|
||||
- [ ] Step 10: 启动工作流 (POST #8) 或运行单节点 (POST #11)
|
||||
- [ ] Step 11: 查询任务状态 (GET #10) 确认完成
|
||||
```
|
||||
|
||||
### 典型 5 物料并发加热工作流示例
|
||||
|
||||
```
|
||||
prepare_materials (count=5)
|
||||
├─ channel_1 → move_to_heating_station (material_number=1) → start_heating → move_to_output
|
||||
├─ channel_2 → move_to_heating_station (material_number=2) → start_heating → move_to_output
|
||||
├─ channel_3 → move_to_heating_station (material_number=3) → start_heating → move_to_output
|
||||
├─ channel_4 → move_to_heating_station (material_number=4) → start_heating → move_to_output
|
||||
└─ channel_5 → move_to_heating_station (material_number=5) → start_heating → move_to_output
|
||||
```
|
||||
|
||||
创建节点时,`prepare_materials` 的 5 个 output handle(`channel_1` ~ `channel_5`)分别连接到 5 个 `move_to_heating_station` 节点的 `material_input` handle。每个 `move_to_heating_station` 的 `heating_station_output` 和 `material_number_output` 连接到对应 `start_heating` 的 `station_id_input` 和 `material_number_input`。
|
||||
@@ -1,76 +0,0 @@
|
||||
# Action Index — virtual_workbench
|
||||
|
||||
6 个动作,按功能分类。每个动作的完整 JSON Schema 在 `actions/<name>.json`。
|
||||
|
||||
---
|
||||
|
||||
## 物料准备
|
||||
|
||||
### `auto-prepare_materials`
|
||||
|
||||
批量准备物料(虚拟起始节点),生成 A1-A5 物料编号,输出 5 个 handle 供后续节点使用
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/prepare_materials.json`](actions/prepare_materials.json)
|
||||
- **可选参数**: `count`(物料数量,默认 5)
|
||||
|
||||
---
|
||||
|
||||
## 机械臂 & 加热台操作
|
||||
|
||||
### `auto-move_to_heating_station`
|
||||
|
||||
将物料从 An 位置移动到空闲加热台(竞争机械臂,自动查找空闲加热台)
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/move_to_heating_station.json`](actions/move_to_heating_station.json)
|
||||
- **核心参数**: `material_number`(物料编号,integer)
|
||||
|
||||
### `auto-start_heating`
|
||||
|
||||
启动指定加热台的加热程序(可并行,3 个加热台同时工作)
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/start_heating.json`](actions/start_heating.json)
|
||||
- **核心参数**: `station_id`(加热台 ID),`material_number`(物料编号)
|
||||
|
||||
### `auto-move_to_output`
|
||||
|
||||
将加热完成的物料从加热台移动到输出位置 Cn
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/move_to_output.json`](actions/move_to_output.json)
|
||||
- **核心参数**: `station_id`(加热台 ID),`material_number`(物料编号)
|
||||
|
||||
---
|
||||
|
||||
## 物料转移
|
||||
|
||||
### `transfer`
|
||||
|
||||
异步转移物料到目标设备(通过 ROS 资源转移)
|
||||
|
||||
- **action_type**: `UniLabJsonCommandAsync`
|
||||
- **Schema**: [`actions/transfer.json`](actions/transfer.json)
|
||||
- **核心参数**: `resource`, `target_device`, `mount_resource`
|
||||
- **占位符字段**:
|
||||
- `resource` — **ResourceSlot**,待转移的物料数组 `[{id, name, uuid}, ...]`
|
||||
- `target_device` — **DeviceSlot**,目标设备路径字符串
|
||||
- `mount_resource` — **ResourceSlot**,目标孔位数组 `[{id, name, uuid}, ...]`
|
||||
|
||||
---
|
||||
|
||||
## 人工确认
|
||||
|
||||
### `manual_confirm`
|
||||
|
||||
创建人工确认节点,等待用户手动确认后继续(含物料转移上下文)
|
||||
|
||||
- **action_type**: `UniLabJsonCommand`
|
||||
- **Schema**: [`actions/manual_confirm.json`](actions/manual_confirm.json)
|
||||
- **核心参数**: `resource`, `target_device`, `mount_resource`, `timeout_seconds`, `assignee_user_ids`
|
||||
- **占位符字段**:
|
||||
- `resource` — **ResourceSlot**,物料数组
|
||||
- `target_device` — **DeviceSlot**,目标设备路径
|
||||
- `mount_resource` — **ResourceSlot**,目标孔位数组
|
||||
- `assignee_user_ids` — `unilabos_manual_confirm` 类型
|
||||
@@ -1,270 +0,0 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"resource": "resource",
|
||||
"target_device": "target_device",
|
||||
"mount_resource": "mount_resource",
|
||||
"timeout_seconds": "timeout_seconds",
|
||||
"assignee_user_ids": "assignee_user_ids"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "resource"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"target_device": {
|
||||
"type": "string",
|
||||
"description": "device reference"
|
||||
},
|
||||
"mount_resource": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "mount_resource"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"timeout_seconds": {
|
||||
"type": "integer"
|
||||
},
|
||||
"assignee_user_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"resource",
|
||||
"target_device",
|
||||
"mount_resource",
|
||||
"timeout_seconds",
|
||||
"assignee_user_ids"
|
||||
],
|
||||
"_unilabos_placeholder_info": {
|
||||
"resource": "unilabos_resources",
|
||||
"target_device": "unilabos_devices",
|
||||
"mount_resource": "unilabos_resources",
|
||||
"assignee_user_ids": "unilabos_manual_confirm"
|
||||
}
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {
|
||||
"resource": "unilabos_resources",
|
||||
"target_device": "unilabos_devices",
|
||||
"mount_resource": "unilabos_resources",
|
||||
"assignee_user_ids": "unilabos_manual_confirm"
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"material_number": "material_number"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"material_number": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"material_number"
|
||||
]
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"station_id": "station_id",
|
||||
"material_number": "material_number"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"station_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"material_number": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"station_id",
|
||||
"material_number"
|
||||
]
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"count": "count"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"default": 5
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
},
|
||||
"goal_default": {
|
||||
"count": 5
|
||||
},
|
||||
"placeholder_keys": {}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"type": "UniLabJsonCommand",
|
||||
"goal": {
|
||||
"station_id": "station_id",
|
||||
"material_number": "material_number"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"station_id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"material_number": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"station_id",
|
||||
"material_number"
|
||||
]
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {}
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
{
|
||||
"type": "UniLabJsonCommandAsync",
|
||||
"goal": {
|
||||
"resource": "resource",
|
||||
"target_device": "target_device",
|
||||
"mount_resource": "mount_resource"
|
||||
},
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resource": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "resource"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"target_device": {
|
||||
"type": "string",
|
||||
"description": "device reference"
|
||||
},
|
||||
"mount_resource": {
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"sample_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"category": {
|
||||
"type": "string"
|
||||
},
|
||||
"pose": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z"
|
||||
],
|
||||
"title": "position",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"orientation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"z": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"minimum": -1.7976931348623157e+308,
|
||||
"maximum": 1.7976931348623157e+308
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"w"
|
||||
],
|
||||
"title": "orientation",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"position",
|
||||
"orientation"
|
||||
],
|
||||
"title": "pose",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"config": {
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": "mount_resource"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"resource",
|
||||
"target_device",
|
||||
"mount_resource"
|
||||
],
|
||||
"_unilabos_placeholder_info": {
|
||||
"resource": "unilabos_resources",
|
||||
"target_device": "unilabos_devices",
|
||||
"mount_resource": "unilabos_resources"
|
||||
}
|
||||
},
|
||||
"goal_default": {},
|
||||
"placeholder_keys": {
|
||||
"resource": "unilabos_resources",
|
||||
"target_device": "unilabos_devices",
|
||||
"mount_resource": "unilabos_resources"
|
||||
}
|
||||
}
|
||||
@@ -1,483 +0,0 @@
|
||||
---
|
||||
name: yibin-electrolyte-submit
|
||||
description: >-
|
||||
通过 Uni-Lab Notebook API 向宜宾电解液工站提交实验,覆盖配液分液(Bioyond LIMS)、
|
||||
扣电组装(CoinCellAssembly)、扣电测试全流程。
|
||||
包含 Excel 解析、formulation 构建、工作流节点参数填写、notebook 提交与状态轮询。
|
||||
Use when the user wants to submit electrolyte experiments, assemble or test coin cells,
|
||||
parse experiment Excel files, build notebook payloads, or mentions
|
||||
宜宾/配液/分液/扣电/电解液实验/notebook提交/CoinCell/BioyondLIMS.
|
||||
---
|
||||
|
||||
# 宜宾电解液产线 API 操作指南
|
||||
|
||||
本 skill 覆盖两个设备的完整操作流程:
|
||||
1. **配液分液工站** (`bioyond_cell_workstation`) — Bioyond LIMS 配液/分液/转运
|
||||
2. **扣电组装站** (`BatteryStation`) — Modbus PLC 扣电组装/数据采集
|
||||
|
||||
## 设备信息
|
||||
|
||||
| 属性 | 配液分液工站 | 扣电组装站 |
|
||||
|------|------------|-----------|
|
||||
| device_id | `bioyond_cell_workstation` | `BatteryStation` |
|
||||
| 显示名 | 配液分液工站 | 扣电工作站 |
|
||||
| 源码 | `unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py` | `unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py` |
|
||||
| 类名 | `BioyondCellWorkstation` | `CoinCellAssemblyWorkstation` |
|
||||
| 通讯 | HTTP REST (Bioyond LIMS API) | Modbus TCP (PLC 寄存器) |
|
||||
|
||||
## 前置条件
|
||||
|
||||
### 认证信息
|
||||
|
||||
```
|
||||
AUTH="Authorization: Lab OTdlY2FkNmUtZmZmMi00YjhiLThhOWEtNWM5ODAyOTJmOTUxOmU0OGM2YWJkLTA4ZmEtNDFjMy04NzhhLTc4M2FiODlhZjYxMw=="
|
||||
BASE="https://uni-lab.test.bohrium.com"
|
||||
```
|
||||
|
||||
来源:`--ak 97ecad6e-fff2-4b8b-8a9a-5c980292f951 --sk e48c6abd-08fa-41c3-878a-783ab89af613 --addr test`
|
||||
|
||||
### 启动 unilab(云端模式)
|
||||
|
||||
> **重要**:提交实验前必须确保 unilab 正在运行且已连接云端 WebSocket。
|
||||
|
||||
```powershell
|
||||
$env:PYTHONIOENCODING="utf-8"
|
||||
conda activate newunilab2603
|
||||
cd D:\UniLabdev\Uni-Lab-OS\unilabos\devices\workstation
|
||||
unilab -g D:\UniLabdev\Uni-Lab-OS\yibin_electrolyte_config.json --ak 97ecad6e-fff2-4b8b-8a9a-5c980292f951 --sk e48c6abd-08fa-41c3-878a-783ab89af613 --upload_registry --addr test --disable_browser --skip_env_check
|
||||
```
|
||||
|
||||
**启动要点**:
|
||||
1. 必须先激活虚拟环境 `newunilab2603`
|
||||
2. 工作目录切到 `unilabos/devices/workstation`(设备驱动所在目录)
|
||||
3. `--upload_registry` 将 64 个设备 + 142 个资源注册到云端
|
||||
4. `--skip_env_check` + `PYTHONIOENCODING=utf-8` 避免 Windows GBK 编码崩溃
|
||||
5. 启动后后台运行,等待日志出现 `Application startup complete` 和 `Host node ready signal published with 3 devices`
|
||||
|
||||
**验证连接成功的标志**:
|
||||
- 日志出现 `[MessageProcessor] ... wss://uni-lab.test.bohrium.com/api/v1/ws/schedule`
|
||||
- 日志出现 `[WebSocketClient] Host node ready signal published with 3 devices`
|
||||
- 日志出现 `Resource tree add completed`(资源树同步完成)
|
||||
|
||||
### 云端物料上架与入库(启动后必做)
|
||||
|
||||
> **在提交实验之前,必须提醒用户完成以下云端操作,否则实验会因物料缺失而失败。**
|
||||
|
||||
1. **拖拽上料**:在云端 UI(`$BASE/laboratory/<lab_uuid>`)的资源树视图中,将物料拖拽到对应的仓库/库位上。unilab 启动后资源树会自动同步到云端,但物料的**上架位置**需要用户在 UI 上手动确认或调整。
|
||||
|
||||
2. **确认配液物料入库**:确保所有配液实验需要的试剂(如 LiPF6、EC、DMC、EMC 等)已在 LIMS 系统中完成入库。可通过以下方式验证:
|
||||
- 云端 UI 资源树中对应仓库(如"粉末加样头堆栈"、"配液站内试剂仓库")下有物料节点
|
||||
- 或通过 API #8 获取资源树后检查物料节点是否存在
|
||||
|
||||
3. **告知 AI 可以提交**:用户完成上述操作后,告知 AI "物料已上架,可以提交实验",AI 再执行 notebook 提交流程。
|
||||
|
||||
**提醒话术模板**(AI 应在启动成功后发送给用户):
|
||||
```
|
||||
unilab 已成功启动并连接云端。提交实验前请完成以下操作:
|
||||
1. 在云端 UI 上确认资源树中的物料位置,必要时拖拽调整上料位
|
||||
2. 确保配液所需的试剂(粉末、液体)已在 LIMS 中完成入库
|
||||
3. 完成后告诉我,我将为您提交实验
|
||||
```
|
||||
|
||||
### 生成 Action Schema(首次使用)
|
||||
|
||||
启动 unilab 后,在 `unilabos_data/` 目录下会生成 `req_device_registry_upload.json`。运行以下命令提取两个设备的 action JSON:
|
||||
|
||||
```bash
|
||||
python .cursor/skills/create-device-skill/scripts/extract_device_actions.py --registry unilabos_data/req_device_registry_upload.json bioyond_cell_workstation .cursor/skills/yibin-electrolyte-submit/actions/
|
||||
python .cursor/skills/create-device-skill/scripts/extract_device_actions.py --registry unilabos_data/req_device_registry_upload.json BatteryStation .cursor/skills/yibin-electrolyte-submit/actions/
|
||||
```
|
||||
|
||||
## 请求约定
|
||||
|
||||
- Windows 平台**必须用 `curl.exe`**(非 PowerShell 的 curl 别名)
|
||||
- 所有请求带 `$AUTH` 头
|
||||
- URL 格式:`$BASE/api/v1/<endpoint>`
|
||||
- POST/PATCH 请求体写入临时 JSON 文件后用 `-d '@tmp.json'` 传参(避免 PowerShell 转义问题)
|
||||
- 本地 API 基址:`http://127.0.0.1:8002/api/v1/`
|
||||
|
||||
## Session State
|
||||
|
||||
每次会话开始时,依次获取以下信息:
|
||||
|
||||
```bash
|
||||
# 1. lab_uuid
|
||||
curl.exe -s -X GET "$BASE/api/v1/edge/lab/info" -H "$AUTH"
|
||||
# → data.uuid → $lab_uuid
|
||||
|
||||
# 2. project_uuid
|
||||
curl.exe -s -X GET "$BASE/api/v1/lab/project/list?lab_uuid=$lab_uuid" -H "$AUTH"
|
||||
# → data.items[].uuid/name → 让用户选择或取唯一项 → $project_uuid
|
||||
```
|
||||
|
||||
## 工作流模板(重要)
|
||||
|
||||
> **必须向用户索要已有的工作流模板 UUID 或 URL,不要自行创建。**
|
||||
>
|
||||
> 原因:通过 `edge/workflow/node` API 创建节点会报 `resource_node_template not found`——
|
||||
> 云端的工作流节点模板系统和设备注册表是独立的,需要用户在云端 UI 上预先配置好工作流模板。
|
||||
|
||||
**获取方式**:
|
||||
- 用户提供工作流页面 URL,如 `$BASE/laboratory/<lab_uuid>/workflow/<workflow_uuid>`
|
||||
- 从 URL 中提取 `workflow_uuid`
|
||||
- 用 API 获取模板详情:
|
||||
|
||||
```
|
||||
GET /api/v1/lab/workflow/template/detail/<workflow_uuid>
|
||||
```
|
||||
|
||||
返回 `data.nodes[]`:每个节点的 uuid、name、param、device_name、handles、disabled。
|
||||
|
||||
**示例**:
|
||||
```
|
||||
工作流 URL: https://uni-lab.test.bohrium.com/laboratory/e9ed9102-d709-4741-b7a0-d1e8578e2065/workflow/b49f80d9-58d6-4456-a521-56f4dd39cda0
|
||||
→ workflow_uuid = b49f80d9-58d6-4456-a521-56f4dd39cda0
|
||||
```
|
||||
|
||||
从模板详情中提取**未 disabled** 的节点的 `uuid` 和 `name`,后续提交 notebook 时使用。
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### #1 获取 lab_uuid
|
||||
|
||||
```
|
||||
GET /api/v1/edge/lab/info
|
||||
```
|
||||
|
||||
### #2 列出项目
|
||||
|
||||
```
|
||||
GET /api/v1/lab/project/list?lab_uuid=$lab_uuid
|
||||
```
|
||||
|
||||
返回 `data.items[]`,取 `uuid` 和 `name`。
|
||||
|
||||
### #3 获取工作流模板详情
|
||||
|
||||
```
|
||||
GET /api/v1/lab/workflow/template/detail/<workflow_uuid>
|
||||
```
|
||||
|
||||
返回 `data.nodes[]`:每个节点的 uuid、name、param、device_name、handles。
|
||||
提取活跃节点(`disabled != true`)的 `uuid` 用于构建 notebook 请求。
|
||||
|
||||
### #4 提交实验(创建 notebook)— 核心 API
|
||||
|
||||
```
|
||||
POST /api/v1/lab/notebook
|
||||
Body: {
|
||||
"lab_uuid": "<lab_uuid>",
|
||||
"project_uuid": "<project_uuid>",
|
||||
"workflow_uuid": "<workflow_uuid>",
|
||||
"name": "<实验名称>",
|
||||
"node_params": [
|
||||
{
|
||||
"sample_uuids": [],
|
||||
"datas": [
|
||||
{
|
||||
"node_uuid": "<模板中的节点UUID>",
|
||||
"param": { <参数键值对> },
|
||||
"sample_params": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**关键注意事项**:
|
||||
- `node_params` 是数组,每个元素代表一轮实验
|
||||
- `datas` 中每个节点对应模板中的一个活跃节点
|
||||
- `param` 中的字段名**必须使用 Python 函数参数名**,不能用模板中存储的 LIMS 字段名(见下方映射表)
|
||||
|
||||
### #5 查询 notebook 状态
|
||||
|
||||
```
|
||||
GET /api/v1/lab/notebook/status?uuid=<notebook_uuid>
|
||||
```
|
||||
|
||||
| status | 含义 |
|
||||
|--------|------|
|
||||
| `running` | 执行中 |
|
||||
| `success` | 成功 |
|
||||
| `fail` | 失败 |
|
||||
|
||||
### #6 运行设备单动作(本地 API)
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:8002/api/v1/job/add
|
||||
Body: {
|
||||
"device_id": "<device_id>",
|
||||
"action": "<action_name>",
|
||||
"action_args": { <参数键值对> },
|
||||
"sample_material": {}
|
||||
}
|
||||
```
|
||||
|
||||
本地 API 可自动解析 `action_type`,无需手动指定。适用于快速调试或云端未连接时。
|
||||
|
||||
### #7 查询本地任务状态
|
||||
|
||||
```
|
||||
GET http://127.0.0.1:8002/api/v1/job/<job_id>/status
|
||||
```
|
||||
|
||||
| status | 含义 |
|
||||
|--------|------|
|
||||
| 0 | UNKNOWN |
|
||||
| 1 | ACCEPTED |
|
||||
| 2 | EXECUTING |
|
||||
| 4 | SUCCEEDED |
|
||||
| 5 | CANCELED |
|
||||
| 6 | ABORTED |
|
||||
|
||||
### #8 获取资源树
|
||||
|
||||
```
|
||||
GET /api/v1/lab/material/download/<lab_uuid>
|
||||
```
|
||||
|
||||
返回所有节点(`id`, `name`, `uuid`, `type`, `parent`)。填写 Slot 字段时用此接口筛选节点。
|
||||
|
||||
## Placeholder Slot 填写规则
|
||||
|
||||
action JSON 中 `placeholder_keys` 标记了哪些字段需要填 Slot:
|
||||
|
||||
| placeholder 值 | Slot 类型 | 填写格式 |
|
||||
|---------------|-----------|---------|
|
||||
| `unilabos_resources` | ResourceSlot | `{"id": "/path/name", "name": "name", "uuid": "xxx"}` |
|
||||
| `unilabos_devices` | DeviceSlot | `"/parent/device_name"` 路径字符串 |
|
||||
| `unilabos_nodes` | NodeSlot | `"/parent/node_name"` 路径字符串 |
|
||||
| `unilabos_class` | ClassSlot | `"class_name"` 字符串 |
|
||||
| `unilabos_formulation` | FormulationSlot | `[{well_name, liquids: [{name, volume}]}]` |
|
||||
|
||||
### ResourceSlot 填写
|
||||
|
||||
从 API #8 资源树中筛选**物料**节点:
|
||||
|
||||
```json
|
||||
{"id": "/bioyond_cell_workstation/YB_Bioyond_Deck/自动堆栈-左", "name": "自动堆栈-左", "uuid": "3a19debc-..."}
|
||||
```
|
||||
|
||||
数组字段:`[{id, name, uuid}, ...]`
|
||||
特例:`create_resource` 的 `res_id` 允许填不存在的路径。
|
||||
|
||||
### DeviceSlot 填写
|
||||
|
||||
从资源树筛选 `type=device` 的节点,填路径字符串:
|
||||
|
||||
```
|
||||
"/BatteryStation"
|
||||
"/bioyond_cell_workstation"
|
||||
```
|
||||
|
||||
### FormulationSlot 填写
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"sample_uuid": "",
|
||||
"well_name": "YB_PrepBottle_15mL_Carrier_bottle_A1",
|
||||
"liquids": [
|
||||
{ "name": "LiPF6", "mass": 12.5 },
|
||||
{ "name": "EC", "mass": 50.0 }
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
`well_name` 从资源树中取物料节点的 `name`。
|
||||
|
||||
## 参数名映射(重要的坑)
|
||||
|
||||
> 工作流模板中存储的参数名和 Python 函数实际接受的参数名**不一定相同**。
|
||||
> 提交 notebook 时必须使用 **Python 函数参数名**。
|
||||
|
||||
### `create_orders_formulation` 参数映射
|
||||
|
||||
| 模板中的 param 键 | 实际 Python 参数名 | 说明 |
|
||||
|-------------------|-------------------|------|
|
||||
| `pouch_cell_info` | `pouch_cell_volume` | 软包组装分液体积 (mL) |
|
||||
| `conductivity_info` | `conductivity_volume` | 电导测试分液体积 (mL) |
|
||||
| `load_shedding_info` | `coin_cell_volume` | 扣电组装分液体积 (mL) |
|
||||
| `formulation` | `formulation` | 配方数组(名称一致) |
|
||||
| `batch_id` | `batch_id` | 批次号(名称一致) |
|
||||
| `bottle_type` | `bottle_type` | 配液瓶类型(名称一致) |
|
||||
| `mix_time` | `mix_time` | 混匀时间(秒)(名称一致) |
|
||||
| `conductivity_bottle_count` | `conductivity_bottle_count` | 电导瓶数(名称一致) |
|
||||
|
||||
当从模板中读到 `param` 包含 `pouch_cell_info` 等 LIMS 字段名时,提交 notebook 时要用右列的 Python 函数参数名。否则会报 `TypeError: got an unexpected keyword argument`。
|
||||
|
||||
## 典型工作流
|
||||
|
||||
### 方式一:通过 Notebook API 批量提交(推荐)
|
||||
|
||||
**适用场景**:多组配方的批量实验,云端管理实验记录
|
||||
|
||||
```
|
||||
1. 向用户索要工作流模板 URL(不要自行创建)
|
||||
2. 获取 lab_uuid(API #1)和 project_uuid(API #2)
|
||||
3. 获取工作流模板详情(API #3),提取活跃节点 UUID
|
||||
4. 解析用户提供的 Excel 文件,构建 formulation 数组
|
||||
5. 提交 notebook(API #4)
|
||||
6. 轮询 notebook 状态(API #5)直到完成
|
||||
```
|
||||
|
||||
**Excel 解析规则**:
|
||||
- 全局参数在第一个数据行:`batch_id`、`bottle_type`、`mix_time`、`coin_cell_volume`、`pouch_cell_volume`、`conductivity_volume`、`conductivity_bottle_count`
|
||||
- 配方列从"试剂名1"开始,交替排列:试剂名列 + 质量列(以 `(g)` 结尾)
|
||||
- 每行一个配方,`order_name` = 配方ID列
|
||||
- formulation 中每个配方的 materials 数组只包含 `mass > 0` 的试剂
|
||||
|
||||
**node_params 构建**:所有配方放入同一个 round 的同一个 datas 条目中,因为只有一个节点(`create_orders_formulation`)。
|
||||
|
||||
### 方式二:设备单步操作(本地 API)
|
||||
|
||||
**适用场景**:调试、快速测试
|
||||
|
||||
```
|
||||
1. 确保 unilab 已在本地启动
|
||||
2. 通过 POST http://127.0.0.1:8002/api/v1/job/add 提交任务
|
||||
3. 通过 GET /api/v1/job/<job_id>/status 查询状态
|
||||
```
|
||||
|
||||
### 设备操作流程:配液 → 转运 → 扣电
|
||||
|
||||
```
|
||||
1. [配液站] scheduler_start_and_auto_feeding → 启动调度 + 上料
|
||||
2. [配液站] create_orders_formulation → 创建配液实验(配方输入)
|
||||
3. [配液站] transfer_3_to_2_to_1_auto → 分液瓶板转运到扣电站
|
||||
4. [扣电站] func_pack_device_init_auto_start_combined → 初始化+自动+启动
|
||||
5. [扣电站] func_sendbottle_allpack_multi → 发送瓶数+批量组装
|
||||
```
|
||||
|
||||
## 云端使用心得
|
||||
|
||||
### 环境准备
|
||||
- Windows 必须设置 `$env:PYTHONIOENCODING="utf-8"` 防止编码崩溃
|
||||
- 使用 `--skip_env_check` 跳过依赖检查,加快启动
|
||||
- 工作目录建议在 `unilabos/devices/workstation` 下启动
|
||||
|
||||
### 连接与注册
|
||||
- `--upload_registry` 会自动将设备和资源注册到云端
|
||||
- WebSocket 连接建立后,本地和云端的资源树会自动同步
|
||||
- 注册成功后用户需在云端 UI 完成**物料拖放上架**操作
|
||||
- 如果 unilab 断开重连,资源树会重新同步
|
||||
|
||||
### 工作流模板
|
||||
- **不要自行调用 API 创建工作流或节点**——云端工作流节点模板需要预配置
|
||||
- 始终向用户索要已有的工作流模板 URL
|
||||
- 从 URL 中提取 `workflow_uuid`,通过 API #3 获取详情
|
||||
- 模板中 `disabled: true` 的节点跳过,只处理活跃节点
|
||||
|
||||
### Notebook 实验提交
|
||||
- Notebook 是云端管理实验的标准方式
|
||||
- 一个 notebook 可包含多轮(`node_params` 数组),每轮可包含多组参数
|
||||
- 提交后通过 API #5 轮询状态,LIMS 配液流程通常需要较长时间(8 个配方约 30-60 分钟)
|
||||
- 实验进度可在云端 UI 和本地 unilab 日志中同步查看
|
||||
|
||||
### 常见错误
|
||||
| 错误 | 原因 | 解决 |
|
||||
|------|------|------|
|
||||
| `edge not started error` | unilab 未连接云端 WebSocket | 检查 unilab 是否在运行、重启 |
|
||||
| `resource_node_template not found` | 云端没有该设备的工作流模板 | 向用户索要已有模板,不要自行创建 |
|
||||
| `got an unexpected keyword argument` | 参数名用了模板字段名而非 Python 函数参数名 | 参照上方映射表转换 |
|
||||
| `UnicodeEncodeError: 'gbk'` | Windows 默认编码不支持特殊字符 | 设置 `PYTHONIOENCODING=utf-8` |
|
||||
| `parse parameter error` | 云端 API 字段名错误 | `device_id` (非 `device_name`)、`action` (非 `action_name`)、必须带 `action_type` |
|
||||
|
||||
## 渐进加载策略
|
||||
|
||||
1. 先读本文件了解 API 端点、参数映射和云端注意事项
|
||||
2. 需要具体 action 参数时,读 [action-index.md](action-index.md) 查找 action 名称和核心参数
|
||||
3. 需要完整 schema 时,读 `actions/<action_name>.json`(需先运行提取命令生成)
|
||||
4. 需要理解参数含义时,读设备源码
|
||||
|
||||
## 完整 Notebook 提交 Checklist
|
||||
|
||||
```
|
||||
- [ ] 确认 unilab 已在本地启动并连接云端 WebSocket
|
||||
- [ ] 提醒用户在云端 UI 拖拽上料、确认物料位置
|
||||
- [ ] 提醒用户确认配液所需试剂已在 LIMS 完成入库
|
||||
- [ ] 等待用户确认物料就绪后再继续
|
||||
- [ ] 向用户索要工作流模板 URL → 提取 workflow_uuid
|
||||
- [ ] 获取 lab_uuid(API #1)
|
||||
- [ ] 获取 project_uuid(API #2)
|
||||
- [ ] 获取工作流模板详情(API #3),提取活跃节点 UUID
|
||||
- [ ] 解析用户 Excel 文件 → 构建 formulation + 全局参数
|
||||
- [ ] 注意参数名映射(模板字段名 → Python 函数参数名)
|
||||
- [ ] 提交 notebook(API #4)
|
||||
- [ ] 轮询 notebook 状态(API #5)直到完成
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 真实场景:宜宾产线 Excel 提交提示词模板
|
||||
|
||||
> 以下为已验证可用的标准提示词,适用于配液-分液-扣电全流程。
|
||||
|
||||
### 场景说明
|
||||
|
||||
- unilab 运行在本地 Windows 机器(miniforge 环境),连接云端 WebSocket
|
||||
- AI(Cursor / OpenClaw)在任意设备上,通过云端 API 操作,**不需要本地 127.0.0.1**
|
||||
- 工作流为 5 节点串联:`create_orders_formulation` → `transfer_3_to_2_to_1_auto` → `func_pack_device_init_auto_start_combined` → `func_sendbottle_allpack_multi` → `transfer_1_to_2`
|
||||
|
||||
### 已知固定参数(宜宾产线)
|
||||
|
||||
```
|
||||
BASE = https://uni-lab.test.bohrium.com
|
||||
lab_uuid = e9ed9102-d709-4741-b7a0-d1e8578e2065
|
||||
project = YiBinElectrolyte (bc5224b4-8120-4765-9961-9dfc1802a1f6)
|
||||
workflow = 配液分液formulation全流程 (2bc59938-db79-4415-ac2d-9897ef125f2f)
|
||||
```
|
||||
|
||||
#### 工作流节点 UUID(固定,无需重新查询)
|
||||
|
||||
| 顺序 | action | node_uuid |
|
||||
|------|--------|-----------|
|
||||
| Step1 | auto-create_orders_formulation | `ece6744a-81ac-4ae4-8cd1-1c8eeda1dab6` |
|
||||
| Step2 | auto-transfer_3_to_2_to_1_auto | `1c37a8dd-5ba0-413d-81db-94b9c936a171` |
|
||||
| Step3 | auto-func_pack_device_init_auto_start_combined | `97a676a2-d257-4479-9096-073b40300970` |
|
||||
| Step4 | auto-func_sendbottle_allpack_multi | `cf69017a-d29c-4aad-a63b-309d63dac2e9` |
|
||||
| Step5 | auto-transfer_1_to_2 | `80d1c1aa-dbc3-4601-86b7-5c22a992dd9e` |
|
||||
|
||||
### 标准提示词
|
||||
|
||||
```
|
||||
请使用 yibin-electrolyte-submit skill,提交以下实验:
|
||||
|
||||
工作流模板 URL:https://uni-lab.test.bohrium.com/laboratory/e9ed9102-d709-4741-b7a0-d1e8578e2065/workflow/2bc59938-db79-4415-ac2d-9897ef125f2f
|
||||
Excel 文件路径:<粘贴或上传 xlsx 路径>
|
||||
|
||||
注意事项:
|
||||
- lab_uuid、project_uuid、workflow节点UUID均已固定,无需重新查询
|
||||
- 直接解析 Excel → 构建 payload → 提交
|
||||
- mix_time 传标量整数即可(已兼容)
|
||||
- 试剂名以 Excel 为准,注意区分 LiDFOB / LiDOFB 等拼写
|
||||
- csv_export_path 取 Excel 中 csv_export_path 列的值
|
||||
- 提交后告知 notebook UUID,无需自动轮询(实验耗时较长)
|
||||
```
|
||||
|
||||
### Excel 列结构说明(experment_template_0415sim-*.xlsx)
|
||||
|
||||
| 列范围 | 内容 |
|
||||
|--------|------|
|
||||
| C | batch_id |
|
||||
| D | bottle_type |
|
||||
| E-H | coin_cell_volume / conductivity_bottle_count / conductivity_volume / csv_export_path |
|
||||
| I-T | 试剂名+质量 交替排列(最多6对)|
|
||||
| U | mix_time |
|
||||
| V | order_name(每行配方的订单号)|
|
||||
| W | pouch_cell_volume |
|
||||
| X-Y | target_device / target_location(Step2参数)|
|
||||
| AA | material_search_enable(Step3参数)|
|
||||
| AB-AS | 扣电站参数(Step4)|
|
||||
|
||||
### CSV 导出说明
|
||||
|
||||
每次 `create_orders_formulation` 完成后,在 `csv_export_path` 目录下生成:
|
||||
```
|
||||
electrolyte_orders_<YYYYMMDD_HHMMSS>.csv
|
||||
```
|
||||
列:`orderCode, orderName, 配液瓶类型, 配液瓶二维码, 分液瓶类型, 分液瓶二维码, 目标配液质量比, 真实配液质量比, 时间`
|
||||
|
||||
> **注意**:barCode 为 `null` 或 `"nullBarCode123456"` 是正常现象,表示 LIMS 中该物料尚未扫码。配液瓶缺失通常是因为物料未放在手动传递窗(`locationId` 前缀 `3a19deae-2c7a-`)。
|
||||
@@ -1,295 +0,0 @@
|
||||
# Action 索引
|
||||
|
||||
> Action JSON 文件需运行提取命令生成,详见 [SKILL.md](SKILL.md) 中「生成 Action Schema」。
|
||||
> 以下描述和参数信息基于源码分析。
|
||||
|
||||
---
|
||||
|
||||
## 配液分液工站 (`bioyond_cell_workstation`)
|
||||
|
||||
源码:`unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py`
|
||||
|
||||
### 调度控制
|
||||
|
||||
#### `scheduler_start`
|
||||
|
||||
启动 Bioyond LIMS 调度系统
|
||||
|
||||
- **核心参数**: 无(仅需 apiKey/requestTime,由设备内部处理)
|
||||
- **返回**: LIMS 响应 `{code, message, data}`
|
||||
|
||||
#### `scheduler_stop`
|
||||
|
||||
停止调度
|
||||
|
||||
- **核心参数**: 无
|
||||
|
||||
#### `scheduler_continue`
|
||||
|
||||
继续调度(从暂停状态恢复)
|
||||
|
||||
- **核心参数**: 无
|
||||
|
||||
#### `scheduler_reset`
|
||||
|
||||
复位调度
|
||||
|
||||
- **核心参数**: 无
|
||||
|
||||
#### `scheduler_start_and_auto_feeding`
|
||||
|
||||
**组合操作**:启动调度 + 自动化上料(4号→3号手套箱)
|
||||
|
||||
- **核心参数**: `xlsx_path`(Excel 物料模板路径,可选)
|
||||
- **可选参数**: WH4 加样头面 12 个点位(materialName + quantity)、WH4 原液瓶面 9 个点位(materialName + quantity + materialType + targetWH)、WH3 人工堆栈 15 个点位(materialType + materialId + quantity)
|
||||
- **流程**: 先 `scheduler_start()`,成功后执行 `auto_feeding4to3()`
|
||||
- **备注**: 支持 Excel 模式和手动参数模式,Excel 路径存在时优先使用 Excel
|
||||
|
||||
### 物料上料/下料
|
||||
|
||||
#### `auto_feeding4to3`
|
||||
|
||||
自动化上料:从 4 号手套箱转运物料到 3 号手套箱
|
||||
|
||||
- **核心参数**: `xlsx_path`(Excel 物料模板路径)
|
||||
- **可选参数**: 同 `scheduler_start_and_auto_feeding` 的 WH4/WH3 点位参数
|
||||
- **返回**: 等待上料任务完成后返回结果
|
||||
|
||||
#### `auto_batch_outbound_from_xlsx`
|
||||
|
||||
自动化下料(从 Excel 读取下料信息)
|
||||
|
||||
- **核心参数**: `xlsx_path`(Excel 下料模板)
|
||||
- **Excel 列**: locationId, warehouseId, 数量, x, y, z
|
||||
|
||||
### 物料管理
|
||||
|
||||
#### `create_and_inbound_materials`
|
||||
|
||||
批量创建固体物料并入库
|
||||
|
||||
- **核心参数**: `material_names`(物料名称列表,默认 `["LiPF6", "LiDFOB", "DTD", "LiFSI", "LiPO2F2"]`)
|
||||
- **可选参数**: `type_id`(物料类型ID), `warehouse_name`(目标仓库,默认 "粉末加样头堆栈")
|
||||
- **流程**: 创建物料 → 批量入库 → 同步
|
||||
|
||||
#### `create_material`
|
||||
|
||||
创建单个物料并可选入库
|
||||
|
||||
- **核心参数**: `material_name`, `type_id`, `warehouse_name`
|
||||
- **可选参数**: `location_name_or_id`(库位编号如 "A01" 或 UUID)
|
||||
|
||||
#### `create_sample`
|
||||
|
||||
创建配液板物料(含子瓶)并入库
|
||||
|
||||
- **核心参数**: `name`, `board_type`(如 "5ml分液瓶板"), `bottle_type`(如 "5ml分液瓶"), `location_code`(如 "A01")
|
||||
- **可选参数**: `warehouse_name`(默认 "手动堆栈")
|
||||
- **备注**: 自动创建 2x4=8 个子瓶
|
||||
|
||||
#### `storage_inbound`
|
||||
|
||||
单个物料入库
|
||||
|
||||
- **核心参数**: `material_id`, `location_id`
|
||||
|
||||
#### `storage_batch_inbound`
|
||||
|
||||
批量物料入库
|
||||
|
||||
- **核心参数**: `items`(`[{materialId, locationId}, ...]`)
|
||||
|
||||
### 配液实验
|
||||
|
||||
#### `create_orders`
|
||||
|
||||
从 Excel 文件创建配液实验订单
|
||||
|
||||
- **核心参数**: `xlsx_path`(Excel 文件路径)
|
||||
- **Excel 列**: 配方ID, 创建日期, 配液瓶类型, 混匀时间(s), 扣电组装分液体积, 软包组装分液体积, 电导测试分液体积, 电导测试分液瓶数, 以及所有以 `(g)` 结尾的物料列
|
||||
- **流程**: 解析 Excel → 提交订单 → 等待全部完成 → 计算质量比 → 提取分液瓶板 → 创建资源树对象
|
||||
- **返回**: `{status, total_orders, bottle_count, reports, mass_ratios, vial_plates}`
|
||||
|
||||
#### `create_orders_formulation`
|
||||
|
||||
从配方列表创建配液实验订单(前端/API 输入版本)
|
||||
|
||||
- **核心参数**: `formulation`(配方数组)
|
||||
- **可选参数**: `batch_id`, `bottle_type`(默认 "配液小瓶"), `mix_time`(秒,列表), `coin_cell_volume`, `pouch_cell_volume`, `conductivity_volume`, `conductivity_bottle_count`
|
||||
- **formulation 格式**:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"order_name": "配方A",
|
||||
"materials": [
|
||||
{"name": "LiPF6", "mass": 12.5},
|
||||
{"name": "EC", "mass": 50.0},
|
||||
{"name": "DMC", "mass": 37.5}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
- **返回**: 同 `create_orders`
|
||||
|
||||
### 物料转运
|
||||
|
||||
#### `transfer_3_to_2_to_1_auto`
|
||||
|
||||
**自动转运**:从 create_orders 结果中自动定位分液瓶板并转运到目标设备
|
||||
|
||||
- **核心参数**: `vial_plates`(分液瓶板列表,来自 create_orders 返回的 `vial_plates`)
|
||||
- **可选参数**: `target_device`(默认 "BatteryStation"), `target_location`(默认 "bottle_rack_6x2"), `mass_ratios`(配方信息)
|
||||
- **流程**: 遍历瓶板 → 解析 locationId → 调用 LIMS 转运 API → 更新资源树
|
||||
- **返回**: `{total, success, failed, results}`
|
||||
|
||||
#### `transfer_3_to_2_to_1`
|
||||
|
||||
3→2→1 物料转运(手动指定坐标)
|
||||
|
||||
- **核心参数**: `source_wh_id`, `source_x`, `source_y`, `source_z`
|
||||
|
||||
#### `transfer_3_to_2`
|
||||
|
||||
3→2 物料转运
|
||||
|
||||
- **核心参数**: `source_wh_id`, `source_x`, `source_y`, `source_z`
|
||||
|
||||
#### `transfer_1_to_2`
|
||||
|
||||
1→2 物料转运
|
||||
|
||||
- **核心参数**: 无
|
||||
|
||||
### 查询
|
||||
|
||||
#### `order_list_v2`
|
||||
|
||||
批量查询实验报告
|
||||
|
||||
- **可选参数**: `timeType`, `beginTime`, `endTime`, `status`(60=运行中, 80=完成, 90=失败), `filter`, `skipCount`, `pageCount`, `sorting`
|
||||
|
||||
---
|
||||
|
||||
## 扣电组装站 (`BatteryStation`)
|
||||
|
||||
源码:`unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py`
|
||||
|
||||
### 设备控制(组合操作)
|
||||
|
||||
#### `func_pack_device_init_auto_start_combined`
|
||||
|
||||
**组合操作**:设备初始化 → 物料搜寻确认 → 切换自动模式 → 启动
|
||||
|
||||
- **核心参数**: `material_search_enable`(是否启用物料搜寻,默认 `False`)
|
||||
- **前置检查**: REG_UNILAB_INTERACT=False, COIL_GB_L_IGNORE_CMD=False, 所有握手寄存器无残留
|
||||
- **流程**: 手动模式 → 初始化命令 → 监测物料搜寻弹窗并自动处理 → 自动模式 → 启动
|
||||
- **返回**: `True`/`False`
|
||||
- **备注**: 第一次运行必须调用此函数;后续批次调用 `func_sendbottle_allpack_multi`
|
||||
|
||||
### 批量组装
|
||||
|
||||
#### `func_sendbottle_allpack_multi`
|
||||
|
||||
**发送瓶数 + 批量组装**(适用于第二批次及后续批次)
|
||||
|
||||
- **核心参数**: `elec_num`(电解液瓶数), `elec_use_num`(每瓶组装电池数), `elec_vol`(电解液吸液量 μL,默认 50)
|
||||
- **可选参数**:
|
||||
- 双滴模式:`dual_drop_mode`(bool), `dual_drop_first_volume`(μL), `dual_drop_suction_timing`(bool), `dual_drop_start_timing`(bool)
|
||||
- 组装参数:`assembly_type`(7=不用铝箔垫/8=用), `assembly_pressure`(N,默认 4200)
|
||||
- 物料参数:`fujipian_panshu`, `fujipian_juzhendianwei`, `gemopanshu`, `gemo_juzhendianwei`, `qiangtou_juzhendianwei`
|
||||
- 开关:`lvbodian`(铝箔垫片), `battery_pressure_mode`(压力模式), `battery_clean_ignore`(忽略清洁)
|
||||
- 其他:`file_path`(CSV保存路径), `formulations`(配方信息,用于CSV追溯)
|
||||
- **流程**: 发送瓶数触发物料搬运 → 设置PLC参数 → 循环(等待PLC请求→下发参数→读取电池数据→写入CSV→更新资源树)→ 完成握手
|
||||
- **返回**: `{success, total_batteries, batteries, summary}`
|
||||
- **备注**: 设备已初始化后直接调用;`formulations` 来自 create_orders 的 `mass_ratios`
|
||||
|
||||
#### `func_allpack_cmd`
|
||||
|
||||
全套组装(基础版本,含断点续传)
|
||||
|
||||
- **核心参数**: `elec_num`, `elec_use_num`, `elec_vol`, `assembly_type`, `assembly_pressure`, `file_path`
|
||||
- **返回**: `{success, total_batteries, batteries, summary}`
|
||||
|
||||
#### `func_allpack_cmd_simp`
|
||||
|
||||
增强版组装(含双滴模式 + 负极片/隔膜/枪头参数)
|
||||
|
||||
- **核心参数**: 同 `func_sendbottle_allpack_multi`
|
||||
- **备注**: 被 `func_sendbottle_allpack_multi` 内部调用
|
||||
|
||||
### 设备控制(单步操作)
|
||||
|
||||
#### `func_pack_device_init`
|
||||
|
||||
设备初始化(手动模式 → 初始化 → 复位标志)
|
||||
|
||||
#### `func_pack_device_auto`
|
||||
|
||||
切换自动模式
|
||||
|
||||
#### `func_pack_device_start`
|
||||
|
||||
启动设备
|
||||
|
||||
#### `func_pack_device_stop`
|
||||
|
||||
设备停止
|
||||
|
||||
#### `func_pack_send_bottle_num`
|
||||
|
||||
发送电解液瓶数(触发物料搬运)
|
||||
|
||||
- **核心参数**: `bottle_num`(瓶数)
|
||||
|
||||
### PLC 参数设置
|
||||
|
||||
#### `qiming_coin_cell_code`
|
||||
|
||||
设置组装物料参数
|
||||
|
||||
- **核心参数**: `fujipian_panshu`(负极片盘数)
|
||||
- **可选参数**: `fujipian_juzhendianwei`, `gemopanshu`, `gemo_juzhendianwei`, `lvbodian`, `battery_pressure_mode`, `battery_pressure`, `battery_clean_ignore`
|
||||
|
||||
### 数据采集
|
||||
|
||||
#### `func_read_data_and_output`
|
||||
|
||||
持续数据采集并导出 CSV(后台循环运行)
|
||||
|
||||
- **核心参数**: `file_path`(CSV 保存目录)
|
||||
- **采集字段**: 开路电压, 极片质量, 组装时间, 压制力, 电解液加注量, 电池类型, 电解液二维码, 电池二维码
|
||||
|
||||
#### `func_stop_read_data`
|
||||
|
||||
停止 CSV 数据采集
|
||||
|
||||
### 设备状态属性(只读)
|
||||
|
||||
| 属性 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `sys_status` | str | 设备状态(启动中/停止中/复位中/初始化中) |
|
||||
| `sys_mode` | str | 设备模式(手动/自动) |
|
||||
| `data_assembly_coin_cell_num` | int | 已完成电池数量 |
|
||||
| `data_assembly_time` | float | 单颗电池组装时间(秒) |
|
||||
| `data_open_circuit_voltage` | float | 开路电压(V) |
|
||||
| `data_pole_weight` | float | 正极片称重(g) |
|
||||
| `data_glove_box_pressure` | float | 手套箱压力(mbar) |
|
||||
| `data_glove_box_o2_content` | float | 手套箱氧含量(ppm) |
|
||||
| `data_glove_box_water_content` | float | 手套箱水含量(ppm) |
|
||||
| `data_coin_cell_code` | str | 电池二维码 |
|
||||
| `data_electrolyte_code` | str | 电解液二维码 |
|
||||
|
||||
---
|
||||
|
||||
## 配置参考
|
||||
|
||||
设备图文件 `yibin_electrolyte_config.json` 中的仓库映射(`warehouse_mapping`):
|
||||
|
||||
| 仓库名称 | 说明 | 典型操作 |
|
||||
|---------|------|---------|
|
||||
| 粉末加样头堆栈 | 20 个点位 (A01-T01) | `create_and_inbound_materials` 入库目标 |
|
||||
| 配液站内试剂仓库 | 9 个点位 (A01-C03) | 试剂存储 |
|
||||
| 自动堆栈-左 | 4 个点位 | 分液瓶板存放,`transfer_3_to_2_to_1_auto` 的源位置 |
|
||||
| 自动堆栈-右 | 4 个点位 | 分液瓶板存放 |
|
||||
| 手动传递窗左/右 | 各 15 个点位 | 人工上料/下料 |
|
||||
| 4号手套箱内部堆栈 | 12 个点位 | `auto_feeding4to3` 的源位置 |
|
||||
26
.cursorignore
Normal file
26
.cursorignore
Normal file
@@ -0,0 +1,26 @@
|
||||
.conda
|
||||
# .github
|
||||
.idea
|
||||
# .vscode
|
||||
output
|
||||
pylabrobot_repo
|
||||
recipes
|
||||
scripts
|
||||
service
|
||||
temp
|
||||
# unilabos/test
|
||||
# unilabos/app/web
|
||||
unilabos/device_mesh
|
||||
unilabos_data
|
||||
unilabos_msgs
|
||||
unilabos.egg-info
|
||||
CONTRIBUTORS
|
||||
# LICENSE
|
||||
MANIFEST.in
|
||||
pyrightconfig.json
|
||||
# README.md
|
||||
# README_zh.md
|
||||
setup.py
|
||||
setup.cfg
|
||||
.gitattrubutes
|
||||
**/__pycache__
|
||||
19
.github/dependabot.yml
vendored
19
.github/dependabot.yml
vendored
@@ -1,19 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
# GitHub Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
target-branch: "dev"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "06:00"
|
||||
open-pull-requests-limit: 5
|
||||
reviewers:
|
||||
- "msgcenterpy-team"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github-actions"
|
||||
commit-message:
|
||||
prefix: "ci"
|
||||
include: "scope"
|
||||
13
.github/workflows/multi-platform-build.yml
vendored
13
.github/workflows/multi-platform-build.yml
vendored
@@ -105,7 +105,6 @@ jobs:
|
||||
with:
|
||||
miniforge-version: latest
|
||||
use-mamba: true
|
||||
python-version: '3.11.14'
|
||||
channels: conda-forge,robostack-staging
|
||||
channel-priority: strict
|
||||
activate-environment: build-env
|
||||
@@ -115,15 +114,13 @@ jobs:
|
||||
- name: Install rattler-build and anaconda-client
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
mamba install -n build-env --override-channels -c conda-forge rattler-build anaconda-client -y
|
||||
mamba install --override-channels -c conda-forge rattler-build anaconda-client -y
|
||||
|
||||
- name: Show environment info
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
conda info
|
||||
conda list -n build-env | grep -E "(rattler-build|anaconda-client)"
|
||||
conda run -n build-env rattler-build --version
|
||||
conda run -n build-env anaconda --version
|
||||
conda list | grep -E "(rattler-build|anaconda-client)"
|
||||
echo "Platform: ${{ matrix.platform }}"
|
||||
echo "OS: ${{ matrix.os }}"
|
||||
|
||||
@@ -131,9 +128,9 @@ jobs:
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
if [[ "${{ matrix.platform }}" == "osx-arm64" ]]; then
|
||||
conda run -n build-env rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
|
||||
rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
|
||||
else
|
||||
conda run -n build-env rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
|
||||
rattler-build build -r ./recipes/msgs/recipe.yaml -c robostack -c robostack-staging -c conda-forge
|
||||
fi
|
||||
|
||||
- name: List built packages
|
||||
@@ -174,5 +171,5 @@ jobs:
|
||||
run: |
|
||||
for package in $(find ./output -name "*.conda"); do
|
||||
echo "Uploading $package to unilab organization..."
|
||||
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
done
|
||||
|
||||
19
.github/workflows/unilabos-conda-build.yml
vendored
19
.github/workflows/unilabos-conda-build.yml
vendored
@@ -98,7 +98,6 @@ jobs:
|
||||
with:
|
||||
miniforge-version: latest
|
||||
use-mamba: true
|
||||
python-version: '3.11.14'
|
||||
channels: conda-forge,robostack-staging,uni-lab
|
||||
channel-priority: strict
|
||||
activate-environment: build-env
|
||||
@@ -108,15 +107,13 @@ jobs:
|
||||
- name: Install rattler-build and anaconda-client
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
mamba install -n build-env --override-channels -c conda-forge rattler-build anaconda-client -y
|
||||
mamba install --override-channels -c conda-forge rattler-build anaconda-client -y
|
||||
|
||||
- name: Show environment info
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
conda info
|
||||
conda list -n build-env | grep -E "(rattler-build|anaconda-client)"
|
||||
conda run -n build-env rattler-build --version
|
||||
conda run -n build-env anaconda --version
|
||||
conda list | grep -E "(rattler-build|anaconda-client)"
|
||||
echo "Platform: ${{ matrix.platform }}"
|
||||
echo "OS: ${{ matrix.os }}"
|
||||
echo "Build full package: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build_full == 'true' }}"
|
||||
@@ -131,7 +128,7 @@ jobs:
|
||||
if: steps.should_build.outputs.should_build == 'true'
|
||||
run: |
|
||||
echo "Building unilabos-env (conda environment dependencies)..."
|
||||
conda run -n build-env rattler-build build -r .conda/environment/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge
|
||||
rattler-build build -r .conda/environment/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge
|
||||
|
||||
- name: Upload unilabos-env to Anaconda.org (if enabled)
|
||||
if: |
|
||||
@@ -143,7 +140,7 @@ jobs:
|
||||
run: |
|
||||
echo "Uploading unilabos-env to uni-lab organization..."
|
||||
for package in $(find ./output -name "unilabos-env*.conda"); do
|
||||
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
done
|
||||
|
||||
- name: Build unilabos (with pip package)
|
||||
@@ -151,7 +148,7 @@ jobs:
|
||||
run: |
|
||||
echo "Building unilabos package..."
|
||||
# 如果已上传到 Anaconda,从 uni-lab channel 获取 unilabos-env;否则从本地 output 获取
|
||||
conda run -n build-env rattler-build build -r .conda/base/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
|
||||
rattler-build build -r .conda/base/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
|
||||
|
||||
- name: Upload unilabos to Anaconda.org (if enabled)
|
||||
if: |
|
||||
@@ -163,7 +160,7 @@ jobs:
|
||||
run: |
|
||||
echo "Uploading unilabos to uni-lab organization..."
|
||||
for package in $(find ./output -name "unilabos-0*.conda" -o -name "unilabos-[0-9]*.conda"); do
|
||||
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
done
|
||||
|
||||
- name: Build unilabos-full - Only when explicitly requested
|
||||
@@ -173,7 +170,7 @@ jobs:
|
||||
github.event.inputs.build_full == 'true'
|
||||
run: |
|
||||
echo "Building unilabos-full package on ${{ matrix.platform }}..."
|
||||
conda run -n build-env rattler-build build -r .conda/full/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
|
||||
rattler-build build -r .conda/full/recipe.yaml -c uni-lab -c robostack-staging -c conda-forge --channel ./output
|
||||
|
||||
- name: Upload unilabos-full to Anaconda.org (if enabled)
|
||||
if: |
|
||||
@@ -184,7 +181,7 @@ jobs:
|
||||
run: |
|
||||
echo "Uploading unilabos-full to uni-lab organization..."
|
||||
for package in $(find ./output -name "unilabos-full*.conda"); do
|
||||
conda run -n build-env anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
anaconda -t ${{ secrets.ANACONDA_API_TOKEN }} upload --user uni-lab --force "$package"
|
||||
done
|
||||
|
||||
- name: List built packages
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -251,7 +251,6 @@ ros-humble-unilabos-msgs-0.9.13-h6403a04_5.tar.bz2
|
||||
*.bz2
|
||||
test_config.py
|
||||
|
||||
# Local config files with secrets
|
||||
yibin_coin_cell_only_config.json
|
||||
yibin_electrolyte_config.json
|
||||
yibin_electrolyte_only_config.json
|
||||
|
||||
/.claude
|
||||
/.cursor
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
# CSV 导出功能变更概要
|
||||
|
||||
## 修改的文件
|
||||
|
||||
### 1. [bioyond_cell_workstation.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py)
|
||||
|
||||
#### 新增导入
|
||||
- `import csv` 和 `import os`(L14-15)
|
||||
|
||||
#### 新增方法
|
||||
|
||||
| 方法 | 功能 |
|
||||
|------|------|
|
||||
| `_extract_prep_bottle_from_report` | 从 order_finish 报文提取**配液瓶**信息(每订单最多1个) |
|
||||
| `_extract_vial_bottles_from_report` | 从 order_finish 报文提取**分液瓶**信息(每订单可多个,返回数组) |
|
||||
| `_export_order_csv` | 汇总所有信息写入 CSV 文件 |
|
||||
|
||||
#### 配液瓶筛选逻辑 (`_extract_prep_bottle_from_report`)
|
||||
- `typemode="1"`, `realQuantity=1`, `usedQuantity=1`
|
||||
- `locationId` 以 `3a19deae-2c7a-` 开头(手动传递窗)
|
||||
- LIMS API 二次确认:`typeName` 含"配液瓶(小)"或"配液瓶(大)"
|
||||
|
||||
#### 分液瓶筛选逻辑 (`_extract_vial_bottles_from_report`)
|
||||
- `typemode="1"`, `realQuantity=1`, `usedQuantity=1`
|
||||
- `locationId` 以 `3a19debc-84b5-` 或 `3a19debe-5200` 开头(自动堆栈-左/右)
|
||||
- LIMS API 二次确认:`typeName` 为"5ml分液瓶"或"20ml分液瓶"
|
||||
- **返回数组**,支持 1×5ml + n×20ml 的组合
|
||||
|
||||
#### 修改的方法
|
||||
|
||||
| 方法 | 变更 |
|
||||
|------|------|
|
||||
| `_submit_and_wait_orders` | 新增配液瓶+分液瓶提取步骤,将 `prep_bottles` 和 `vial_bottles` 存入 `final_result` |
|
||||
| `create_orders` | 添加 `csv_export_path` 参数,末尾调用 `_export_order_csv` |
|
||||
| `create_orders_formulation` | 添加 `csv_export_path` 参数,末尾调用 `_export_order_csv` |
|
||||
|
||||
#### CSV 输出格式
|
||||
```
|
||||
orderCode, orderName, 配液瓶类型, 配液瓶二维码, 分液瓶类型, 分液瓶二维码, 目标配液质量比, 真实配液质量比, 时间
|
||||
```
|
||||
- 单个分液瓶时直接写值;多个分液瓶时类型和二维码用 JSON 数组表示
|
||||
- CSV 编码使用 `utf-8-sig`(兼容 Excel 打开)
|
||||
- `csv_export_path` 默认为空字符串,不传则不导出(向后兼容)
|
||||
|
||||
---
|
||||
|
||||
### 2. [bioyond_cell.yaml](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/registry/devices/bioyond_cell.yaml)
|
||||
|
||||
为两个 action 注册了 `csv_export_path` 参数:
|
||||
|
||||
- `auto-create_orders`: `goal_default` + `schema.properties.goal.properties` 中添加 `csv_export_path`
|
||||
- `auto-create_orders_formulation`: 同上
|
||||
|
||||
---
|
||||
|
||||
### 3. [coin_cell_assembly.py](file:///d:/UniLabdev/Uni-Lab-OS/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py) 的 CSV 改动与全流程追溯
|
||||
|
||||
在 `bioyond_cell_workstation.py` 的 `_submit_and_wait_orders` 最后阶段,提取 `prep_bottles`(配液瓶)和 `vial_bottles`(分液瓶)的条码并随 `mass_ratios` 数组一起下发给各下游工站(例如扣电组装站),实现跨站的全流程配方追溯。
|
||||
|
||||
并在扣电站生成的 `date_xxx.csv` 中,**替换并新增**了以下列:
|
||||
- 移除了原有的 `formulation_order_code` 与合并的 `formulation_ratio` 列。
|
||||
- 新增 `orderName` 导出
|
||||
- 新增 `prep_bottle_barcode`(奔曜传递的配液瓶二维码)
|
||||
- 新增 `vial_bottle_barcodes`(奔曜传递的分液瓶二维码,多瓶时存 JSON 数组)
|
||||
- 新增 `target_mass_ratio` 理论目标质量比
|
||||
- 新增 `real_mass_ratio` 实际称量真实质量比
|
||||
|
||||
*注意:这与操作人员在手套箱内扫码传入扣电站的 `electrolyte_code` 是单独记录的,方便做数据核对。*
|
||||
|
||||
## 向后兼容性
|
||||
- `csv_export_path` 默认值为 `""`(空字符串),现有调用不受影响
|
||||
- 新增的 `prep_bottles` 和 `vial_bottles` 字段为 `final_result` 和 `mass_ratios` 内部的新增附属字段,不破坏现有数据结构。
|
||||
85
AGENTS.md
85
AGENTS.md
@@ -23,8 +23,11 @@ unilab --skip_env_check # skip auto-install of dependencies
|
||||
unilab --visual rviz|web|disable # visualization mode
|
||||
unilab --is_slave # run as slave node
|
||||
|
||||
# Workflow upload subcommand
|
||||
# Workflow upload subcommand(P6.1 新增 --target_device;P6.1.1 新增 --target_model)
|
||||
unilab workflow_upload -f <workflow.json> -n <name> --tags tag1 tag2
|
||||
unilab workflow_upload -f <workflow.json> --target_device prcxi # P6.1 默认;同上 P6 行为
|
||||
unilab workflow_upload -f <workflow.json> --target_device prcxi --target_model 9320 # P6.1.1:型号粒度
|
||||
unilab workflow_upload -f <workflow.json> --target_device beckman # 未来支持,需在 YAML 中声明 target_devices.beckman
|
||||
|
||||
# Tests
|
||||
pytest tests/ # all tests
|
||||
@@ -72,6 +75,86 @@ pytest tests/resources/test_resourcetreeset.py::TestClassName::test_method # si
|
||||
|
||||
Example device graphs and experiment configs are in `unilabos/test/experiments/` (not `tests/`). Registry test fixtures in `unilabos/test/registry/`.
|
||||
|
||||
### Labware Mapping Table (`labware_mapping.yaml`) — P6 + P6.1 + P6.1.1
|
||||
|
||||
Opentrons → 目标仪器(PRCXI / Beckman / Tecan ...)的「槽位重映射 + labware 归类 +
|
||||
class_name 选择」全部外化到项目根的
|
||||
[`labware_mapping.yaml`](./labware_mapping.yaml)(与 `pyproject.toml` 同级,最显眼的位置)。
|
||||
要新增 SKU、新厂商、新型号、或调整 tip 量程档时,**只改 YAML,不改 Python**。
|
||||
|
||||
- **YAML 两段顶层语义**(P6.1.1 起 `slot_remap` 已下沉到 `target_devices` 内):
|
||||
- `kinds` — 顺序敏感的 regex;把 labware 字符串归到 `trash / tip_rack / tube_rack / plate`。**全局段**,与目标仪器无关。
|
||||
- `target_devices.<name>` — 按目标仪器组织的规则段,内含三个字段:
|
||||
- `slot_remap` — 替代历史 `_map_deck_slot`(例:`4 → 13`、`8 → 14`、`12+trash → 16`)。
|
||||
- `rules` — 顺序敏感的「`kind + hole_count + volume_min/volume_max` → `class_name`」规则,首个命中胜出。
|
||||
- `models.<model_name>` — 可选的型号粒度覆盖(slot_remap / rules);缺失字段自动继承厂商级。
|
||||
- **`target_devices` 内段名约定**:
|
||||
- `default` — **固定段名**,兜底物料集 + 兜底 `slot_remap`。caller 传入的 `target_device` 在 `target_devices`
|
||||
下未声明时,自动 fallback 到此段(loader 单次 warning,下游消费方零感知)。
|
||||
**第一版按 prcxi 内容拷贝填充**(值仍是 `PRCXI_*`),但与 prcxi 段在 YAML 中
|
||||
各自独立,可独立演进。**`default` 不支持 `models` 子段**——型号粒度差异必须落到具体仪器段。
|
||||
- `prcxi` / `beckman` / `tecan` / ... — 具体仪器段(厂商粒度);caller 显式
|
||||
`--target_device <name>` 时命中。可在 `models.<model>` 下声明同厂商不同型号的差异。
|
||||
- **4 段 fallback 链**(`slot_remap` / `rules` 共用):
|
||||
1. `target_devices.<device>.models.<model>.<field>`(caller 同时传 device + model)
|
||||
2. `target_devices.<device>.<field>`(厂商级;步骤 1 缺字段时静默 fallback)
|
||||
3. `target_devices.default.<field>`(caller 传未声明 device,或步骤 2 缺字段;打 warning)
|
||||
4. `_BUILTIN_DEFAULT.target_devices.default.<field>`(YAML 误删 default 段时的最后兜底)
|
||||
- **CLI 用法**:
|
||||
- P6.1:`unilab workflow_upload -f <workflow.json> --target_device prcxi`
|
||||
(`--target_device` snake-case,默认 `prcxi`;未声明的名字自动 fallback 到 `default` 段)。
|
||||
- P6.1.1:可加 `--target_model <name>`(snake,可省略,默认 `None`)。
|
||||
例:`unilab workflow_upload -f <workflow.json> --target_device prcxi --target_model 9320`。
|
||||
- **入口代码**:`unilabos/workflow/labware_mapping.py` 暴露 `remap_slot` / `infer_kind` /
|
||||
`resolve_target_class` / `reload_mapping`。
|
||||
API 签名(P6.1.1):
|
||||
- `remap_slot(raw_slot, object_type="", *, target_device="prcxi", target_model=None)`
|
||||
- `resolve_target_class(target_device, kind, hole_count=None, volume=None, *, target_model=None)`
|
||||
`workflow/common.py` 中 `_map_deck_slot` / `_infer_reagent_kind` /
|
||||
`_apply_tip_rack_class_from_transfer_volumes` / `_apply_target_labware_class_auto_match` /
|
||||
`_reconcile_slot_carrier_target_class` 都已转调 YAML 并透传 `target_device` / `target_model`;
|
||||
YAML 未命中(孔数 / 体积超出 default 段覆盖范围)时 fallback 到
|
||||
`prcxi_labware.get_prcxi_labware_template_specs` 的模板打分匹配,并打 warning 提示「请补到映射表」。
|
||||
- **`labware_info` 字段重命名**:P6 的 `prcxi_class_name` → P6.1 的 `target_class_name`,
|
||||
13 处全部同步刷新;旧 schema(顶层 `vendors` / `slot_remap` 或任一 rule 内 `prcxi_class`)
|
||||
会触发 loader warning 并整段 fallback 到 builtin 默认表。
|
||||
- **测试**:
|
||||
- `pytest tests/workflow/test_labware_mapping.py` —— 45 项单元测试(含 P6.1 + P6.1.1 用例:
|
||||
`test_remap_slot_model_level_overrides_device_level`、
|
||||
`test_remap_slot_model_inherits_device_when_field_missing`、
|
||||
`test_legacy_top_level_slot_remap_rejected`、
|
||||
`test_default_section_models_subsection_warns` 等)。
|
||||
- `pytest tests/workflow/test_build_protocol_graph_target_device.py` —— 6 项集成
|
||||
测试(默认 / 显式 prcxi / unknown 段 fallback / per-device tip class / 字段重命名 /
|
||||
P6.1.1 model-level slot_remap)。
|
||||
- **设计文档**:[`product_designs/protocol_convert/06-labware-mapping-table.md`](../product_designs/protocol_convert/06-labware-mapping-table.md)
|
||||
(§11.7 = P6.1 多目标仪器选择,§11.8 = P6.1.1 槽位映射按厂商+型号分叉)。
|
||||
|
||||
### P2 跨 slot transfer_liquid 合并(v2,已落地)
|
||||
|
||||
当一次 phase 中存在「单源吸取 → 跨多个 plate 分发」(典型 `steps/51b9a5.json` 9 plate × 12 well = 108 条 1:1 dispense),Stage 2 + Stage 3 现在能把它折叠成 **1 个 merged set_liquid_from_plate + 1 个 transfer_liquid** 节点。
|
||||
|
||||
- **Stage 2**([`Protocols/protocol_converter/change_to_transfer_group.py`](../Protocols/protocol_converter/change_to_transfer_group.py)):
|
||||
- `_pair_mergeable` 只要求源 slot / tip 量程档 / use_channels 一致;不再要求 `_target_slot` 相同。
|
||||
- `_merge_two_transfer_actions` 维护 `_target_slots: list[int]`(与 `_target_wells` 平行,每次 dispense 一条)。
|
||||
- `export_transfer_actions` 通过 `_register_target_reagent_key` 统一注册 reagent_key:跨 slot 时按 `_target_slots` 顺序拼出 `action_args.targets: list[str]`(同板退化为 `str`)。
|
||||
- 末尾 `pop` 全部 `_` 前缀字段(包括新增的 `_target_slots`)。
|
||||
- **Stage 3**([`Uni-Lab-OS/unilabos/workflow/common.py`](unilabos/workflow/common.py)):
|
||||
- 新增 `_emit_merged_set_liquid(...)`:对 `params.targets: list[str]` 的 transfer_liquid 节点,在其上游插入一个 **merged `set_liquid_from_plate`** 跨板聚合器;其 `param.wells` 是按 dispense 顺序通过 cursor 走 `reagent[key].well` 得出的有序跨板 well refs;多入边(每 plate 一条 `create_resource.labware → wells_identifier`),单出边(`output_wells → transfer_liquid.targets_identifier`)。
|
||||
- 把 `params["targets"]` 改写为 synthetic str `_merged_targets_<idx>` 并注册 `resource_last_writer`,保证 INPUT_PORT_MAPPING 走 P3 既有的单边路径。
|
||||
- `OUTPUT_PORT_MAPPING` 在原始 `step.param.targets` 为 `list[str]` 时为每个 reagent_key 分别注册 transfer_liquid 的下游 writer。
|
||||
- **PRCXI runtime**([`prcxi/prcxi.py`](unilabos/devices/liquid_handling/prcxi/prcxi.py)):`change_slots` 改为遍历所有 source / target 的 parent plate 并按 plate name 去重(跨板 4 个 plate 都能 `update_pipetting_position`)。
|
||||
- **`liquid_handler_abstract.transfer_liquid`**:**完全不改动**,主循环 `i % num_targets` 与单边 + 单 list 完全兼容。
|
||||
|
||||
CLI 行为不变:现有 `unilab workflow_upload -f <workflow.json> ...` 一切照旧;跨 slot 协议自动走 v2 路径。
|
||||
|
||||
测试:
|
||||
- `pytest Protocols/protocol_converter/tests/test_cross_slot_merge.py` — Stage 2 单测 10 项。
|
||||
- `pytest tests/workflow/test_common_cross_slot_v2.py` — Stage 3 集成测试 6 项。
|
||||
- `pytest tests/devices/liquid_handling/test_set_liquid_from_plate_cross_plate.py` — device 跨板单测 6 项(pylabrobot 不全时优雅 skip)。
|
||||
|
||||
设计文档:[`product_designs/protocol_convert/02-cross-slot-merge.md`](../product_designs/protocol_convert/02-cross-slot-merge.md)(§9 v2 设计 + §11 落地记录)。
|
||||
|
||||
## Code Conventions
|
||||
|
||||
- Code comments and log messages in simplified Chinese
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
# 变更说明 2026-03-24
|
||||
|
||||
## 问题背景
|
||||
|
||||
`BioyondElectrolyteDeck`(原 `BIOYOND_YB_Deck`)迁移后,前端物料未能正常上传/同步。
|
||||
|
||||
---
|
||||
|
||||
## 修复内容
|
||||
|
||||
### 1. `unilabos/resources/bioyond/decks.py`
|
||||
|
||||
- 补回 `setup: bool = False` 参数及 `if setup: self.setup()` 逻辑,与旧版 `BIOYOND_YB_Deck` 保持一致
|
||||
- 工厂函数 `bioyond_electrolyte_deck` 保留显式调用 `deck.setup()`,避免重复初始化
|
||||
|
||||
```python
|
||||
# 修复前(缺少 setup 参数,无法通过 setup=True 触发初始化)
|
||||
def __init__(self, name, size_x, size_y, size_z, category):
|
||||
super().__init__(...)
|
||||
|
||||
# 修复后
|
||||
def __init__(self, name, size_x, size_y, size_z, category, setup: bool = False):
|
||||
super().__init__(...)
|
||||
if setup:
|
||||
self.setup()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `unilabos/resources/graphio.py`
|
||||
|
||||
- 修复 `resource_bioyond_to_plr` 中两处 `bottle.tracker.liquids` 直接赋值导致的崩溃
|
||||
- `ResourceHolder`(如枪头盒的 TipSpot 槽位)没有 `tracker` 属性,直接访问会抛出 `AttributeError`,阻断整个 Bioyond 同步流程
|
||||
|
||||
```python
|
||||
# 修复前
|
||||
bottle.tracker.liquids = [...]
|
||||
|
||||
# 修复后
|
||||
if hasattr(bottle, 'tracker') and bottle.tracker is not None:
|
||||
bottle.tracker.liquids = [...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. `unilabos/app/main.py`
|
||||
|
||||
- 保留 `file_path is not None` 条件不变(已还原),并补充注释说明原因
|
||||
- 该逻辑只在**本地文件模式**下有意义:本地 graph 文件只含设备结构,远端有已保存物料,merge 才能将两者合并
|
||||
- 远端模式(`file_path=None`)下,`resource_tree_set` 和 `request_startup_json` 来自同一份数据,merge 为空操作,条件是否加 `file_path is not None` 对结果没有影响
|
||||
|
||||
---
|
||||
|
||||
### 4. `unilabos/devices/workstation/bioyond_studio/station.py` ⭐ 核心修复
|
||||
|
||||
- 当 deck 通过反序列化创建时,不会自动调用 `setup()`,导致 `deck.children` 为空,`warehouses` 始终是 `{}`
|
||||
- 增加兜底逻辑:仓库扫描后仍为空,则主动调用 `deck.setup()` 初始化仓库
|
||||
- 这是导致所有物料放置失败(`warehouse '...' 在deck中不存在。可用warehouses: []`)的根本原因
|
||||
|
||||
```python
|
||||
# 新增兜底
|
||||
if not self.deck.warehouses and hasattr(self.deck, "setup") and callable(self.deck.setup):
|
||||
logger.info("Deck 无仓库子节点,调用 setup() 初始化仓库")
|
||||
self.deck.setup()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 补充修复 2026-03-25:依华扣电组装工站子物料未上传
|
||||
|
||||
### 问题
|
||||
|
||||
`CoinCellAssemblyWorkstation.post_init` 直接上传空 deck,未调用 `deck.setup()`,导致:
|
||||
- 前端子物料(成品弹夹、料盘、瓶架等)不显示
|
||||
- 运行时 `self.deck.get_resource("成品弹夹")` 抛出 `ResourceNotFoundError`
|
||||
|
||||
### 修复文件
|
||||
|
||||
**`unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py`**
|
||||
- `YihuaCoinCellDeck.__init__` 补回 `setup: bool = False` 参数及 `if setup: self.setup()` 逻辑
|
||||
|
||||
**`unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py`**
|
||||
- `post_init` 中增加与 Bioyond 工站相同的兜底逻辑:deck 无子节点时调用 `deck.setup()` 初始化
|
||||
|
||||
```python
|
||||
# post_init 中新增
|
||||
if self.deck and not self.deck.children and hasattr(self.deck, "setup") and callable(self.deck.setup):
|
||||
logger.info("YihuaCoinCellDeck 无子节点,调用 setup() 初始化")
|
||||
self.deck.setup()
|
||||
```
|
||||
|
||||
### 联动 Bug:`MaterialPlate.create_with_holes` 构造顺序错误
|
||||
|
||||
**现象**:`deck.setup()` 被调用后,启动时抛出:
|
||||
```
|
||||
设备后初始化失败: Must specify either `ordered_items` or `ordering`.
|
||||
```
|
||||
|
||||
**根因**:`create_with_holes` 原来的逻辑是先构造空的 `MaterialPlate` 实例,再 assign 洞位:
|
||||
```python
|
||||
# 旧(错误):cls(...) 时 ordered_items=None → ItemizedResource.__init__ 立即报错
|
||||
plate = cls(name=name, ...) # ← 这里就崩了
|
||||
holes = create_ordered_items_2d(...) # ← 根本没走到这里
|
||||
for hole_name, hole in holes.items():
|
||||
plate.assign_child_resource(...)
|
||||
```
|
||||
pylabrobot 的 `ItemizedResource.__init__` 强制要求 `ordered_items` 和 `ordering` 必须有一个不为 `None`,空构造直接失败。
|
||||
|
||||
**修复**:先建洞位,再作为 `ordered_items` 传给构造函数:
|
||||
```python
|
||||
# 新(正确):先建洞位,再一次性传入构造函数
|
||||
holes = create_ordered_items_2d(klass=MaterialHole, num_items_x=4, ...)
|
||||
return cls(name=name, ..., ordered_items=holes)
|
||||
```
|
||||
|
||||
> 此 bug 此前未被触发,是因为 `deck.setup()` 从未被调用到——正是上面 `post_init` 兜底修复引出的联动问题。
|
||||
|
||||
---
|
||||
|
||||
## 补充修复 2026-03-25:3→2→1 转运资源同步失败
|
||||
|
||||
### 问题
|
||||
|
||||
配液工站(Bioyond)完成分液后,调用 `transfer_3_to_2_to_1_auto` 将分液瓶板转运到扣电工站(BatteryStation)。物理 LIMS 转运成功,但数字孪生资源树同步始终失败:
|
||||
```
|
||||
[资源同步] ❌ 失败: 目标设备 'BatteryStation' 中未找到资源 'bottle_rack_6x2'
|
||||
```
|
||||
|
||||
### 根因
|
||||
|
||||
`_get_resource_from_device` 方法负责跨设备查找资源对象,有两个问题:
|
||||
|
||||
1. **原始路径完全失效**:尝试 `from unilabos.app.ros2_app import get_device_plr_resource_by_name`,但该模块不存在,`ImportError` 被 `except Exception: pass` 静默吞掉
|
||||
2. **降级路径搜错地方**:遍历 `self._plr_resources`(Bioyond 自己的资源),不可能找到 BatteryStation 的 `bottle_rack_6x2`
|
||||
|
||||
### 修复文件
|
||||
|
||||
**`unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py`**
|
||||
|
||||
改用全局设备注册表 `registered_devices` 跨设备访问目标 deck:
|
||||
|
||||
```python
|
||||
# 修复前(失效)
|
||||
from unilabos.app.ros2_app import get_device_plr_resource_by_name # 模块不存在
|
||||
return get_device_plr_resource_by_name(device_id, resource_name)
|
||||
|
||||
# 修复后
|
||||
from unilabos.ros.nodes.base_device_node import registered_devices
|
||||
device_info = registered_devices.get(device_id)
|
||||
if device_info is not None:
|
||||
driver = device_info.get("driver_instance") # TypedDict 是 dict,必须用 .get()
|
||||
if driver is not None:
|
||||
deck = getattr(driver, "deck", None)
|
||||
if deck is not None:
|
||||
res = deck.get_resource(resource_name)
|
||||
```
|
||||
|
||||
关键细节:`DeviceInfoType` 是 `TypedDict`(即普通 `dict`),必须用 `device_info.get("driver_instance")` 而非 `getattr(device_info, "driver_instance", None)`——后者对字典永远返回 `None`。
|
||||
|
||||
---
|
||||
|
||||
## 根本原因分析
|
||||
|
||||
旧版以**本地文件模式**启动(有 `graph` 文件),deck 在启动前已通过 `merge_remote_resources` 获得仓库子节点,反序列化时能正确恢复 warehouses。
|
||||
|
||||
新版以**远端模式**启动(`file_path=None`),deck 反序列化时没有仓库子节点,`station.py` 扫描为空,所有物料的 warehouse 匹配失败,Bioyond 同步的 16 个资源全部无法放置到对应仓库位,前端不显示。
|
||||
1331
docs/moveit2_integration_summary.md
Normal file
1331
docs/moveit2_integration_summary.md
Normal file
File diff suppressed because it is too large
Load Diff
140
labware_mapping.yaml
Normal file
140
labware_mapping.yaml
Normal file
@@ -0,0 +1,140 @@
|
||||
# Opentrons → 目标仪器 物料映射表(P6.1.1)
|
||||
#
|
||||
# 两段顶层 key(P6.1.1 起 slot_remap 从顶层下沉到 target_devices 内):
|
||||
# kinds : labware 字符串 → kind 归类(与目标仪器无关,**保留全局**)
|
||||
# target_devices : 按目标仪器 + 型号组织;rule = kind + hole_count + volume_min/max → class_name;
|
||||
# slot_remap 也内嵌在 target_devices 下(按 deck 物理布局变化)
|
||||
#
|
||||
# target_devices 段内结构:
|
||||
# target_devices.<device>: # 厂商段(必填)
|
||||
# slot_remap: {...} # 厂商级默认 slot 映射(缺失 → 继承 default 段)
|
||||
# rules: [...] # 厂商级规则(缺失 → 继承 default 段)
|
||||
# models: # 同厂商多型号(可选;缺失 = 仅厂商级,不区分型号)
|
||||
# <model_name>: # 型号子段
|
||||
# slot_remap: {...} # 型号级覆盖(缺失 → 继承厂商级)
|
||||
# rules: [...] # 型号级覆盖(缺失 → 继承厂商级)
|
||||
#
|
||||
# 段名约定:
|
||||
# target_devices.default : 兜底物料集 + 兜底 slot_remap。caller 传未声明的 target_device 时使用此段。
|
||||
# **不支持 models 子段**(型号粒度差异必须落到具体仪器段,否则歧义)。
|
||||
# target_devices.<name> : 具体仪器段(prcxi / beckman / tecan ...)。
|
||||
#
|
||||
# 解析链(remap_slot / resolve_target_class 共用,字段级 fallback):
|
||||
# 1. target_devices.<device>.models.<model>.<field> (caller 同时传 device + model)
|
||||
# 2. target_devices.<device>.<field> (caller 传 device,或步骤 1 缺字段)
|
||||
# 3. target_devices.default.<field> (caller 传未声明 device,或步骤 2 缺字段)
|
||||
# 4. _BUILTIN_DEFAULT.target_devices.default.<field> (YAML 误删 default 段时的最后兜底)
|
||||
#
|
||||
# 编辑建议:
|
||||
# 1. 顺序敏感:kinds 与 rules 内首个命中胜出;窄规则在前、宽规则在后。
|
||||
# 2. volume_min / volume_max 是闭区间(µL)。任一字段可省略;都省略 = 不限制体积。
|
||||
# 3. notes 仅作注释,不参与匹配。
|
||||
# 4. 新增目标仪器:复制 target_devices.prcxi 段、改 device 名、改 slot_remap + rules。
|
||||
# 5. 同厂商不同型号:在 target_devices.<device>.models.<model> 下显式覆盖差异字段;
|
||||
# 没声明的字段自动继承厂商级。
|
||||
# 6. P6.1.1 不再支持顶层 slot_remap;检出顶层 slot_remap → warning + fallback 到 builtin。
|
||||
#
|
||||
# 设计文档:product_designs/protocol_convert/06-labware-mapping-table.md(§11.8)
|
||||
|
||||
kinds:
|
||||
# 顺序敏感的 regex;第一个命中胜出
|
||||
# 注意:trash 必须在 tip_rack 之前;tip_rack 必须在 tube_rack 之前("tuberack" 含 "rack")
|
||||
- { pattern: "trash", kind: trash }
|
||||
- { pattern: "tiprack|tip[_ ]?rack|opentrons_\\d+_tiprack", kind: tip_rack }
|
||||
- { pattern: "tuberack|tube[_ ]rack|eppendorf.*rack|safelock.*rack", kind: tube_rack }
|
||||
# 「<labware> 含 'rack' 但不含 'tip'」也归到 tube_rack(与历史 _infer_reagent_kind 行为一致)
|
||||
- { pattern: "(?:^|[^a-z])rack(?:[^a-z]|$)", kind: tube_rack }
|
||||
- { pattern: ".*", kind: plate }
|
||||
|
||||
target_devices:
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# default:兜底物料集 + 兜底 slot_remap。
|
||||
# caller 传未声明的 target_device 时使用本段;**不支持 models 子段**。
|
||||
# 第一版内容按 prcxi 拷贝填充(值仍是 PRCXI_*),但语义独立,可独立演进。
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
default:
|
||||
notes: "默认兜底物料集;caller 传未声明 target_device 时使用此段。第一版按 prcxi 拷贝填充。"
|
||||
slot_remap:
|
||||
# raw slot → deck slot;与对象类型无关
|
||||
default:
|
||||
"4": "13"
|
||||
"8": "14"
|
||||
# 按 object 字段覆盖 default
|
||||
by_object:
|
||||
trash:
|
||||
"12": "16"
|
||||
rules:
|
||||
# ─ tip rack(默认量程档:≤10 / <300 / 否则 1000) ─
|
||||
- { kind: tip_rack, hole_count: 96, volume_max: 10, class_name: PRCXI_10uL_Tips }
|
||||
- { kind: tip_rack, hole_count: 96, volume_max: 299.9, class_name: PRCXI_300ul_Tips }
|
||||
- { kind: tip_rack, hole_count: 96, class_name: PRCXI_1000uL_Tips }
|
||||
# ─ tube rack ─
|
||||
- { kind: tube_rack, hole_count: 24, class_name: PRCXI_EP_Adapter, notes: "Eppendorf 1.5/2 mL 24 位 4×6" }
|
||||
- { kind: tube_rack, hole_count: 10, class_name: PRCXI_EP_Adapter, notes: "Falcon 4x50 + 6x15 mL(10 位兼容 4×6 适配器)" }
|
||||
# ─ plate ─
|
||||
- { kind: plate, hole_count: 96, class_name: PRCXI_BioER_96_wellplate }
|
||||
- { kind: plate, hole_count: 384, class_name: PRCXI_BioER_384_wellplate }
|
||||
# ─ trash ─
|
||||
- { kind: trash, class_name: PRCXI_trash }
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# prcxi:PRCXI 仪器专用段。caller 显式传 --target_device prcxi 时命中此段。
|
||||
# 厂商级 slot_remap + rules 适用于"未声明 model"的调用;
|
||||
# models 子段下声明同厂商不同型号的 deck 物理布局差异。
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
prcxi:
|
||||
slot_remap:
|
||||
# PRCXI 多数型号通用的 deck 物理布局映射
|
||||
default:
|
||||
"4": "13"
|
||||
"8": "14"
|
||||
by_object:
|
||||
trash:
|
||||
"12": "16"
|
||||
rules:
|
||||
# ─ tip rack(PRCXI 量程档:≤10 / <300 / 否则 1000) ─
|
||||
- { kind: tip_rack, hole_count: 96, volume_max: 10, class_name: PRCXI_10uL_Tips }
|
||||
- { kind: tip_rack, hole_count: 96, volume_max: 299.9, class_name: PRCXI_300ul_Tips }
|
||||
- { kind: tip_rack, hole_count: 96, class_name: PRCXI_1000uL_Tips }
|
||||
# ─ tube rack ─
|
||||
- { kind: tube_rack, hole_count: 24, class_name: PRCXI_EP_Adapter, notes: "Eppendorf 1.5/2 mL 24 位 4×6" }
|
||||
- { kind: tube_rack, hole_count: 10, class_name: PRCXI_EP_Adapter, notes: "Falcon 4x50 + 6x15 mL(10 位兼容 4×6 适配器)" }
|
||||
# ─ plate ─
|
||||
- { kind: plate, hole_count: 96, class_name: PRCXI_BioER_96_wellplate }
|
||||
- { kind: plate, hole_count: 384, class_name: PRCXI_BioER_384_wellplate }
|
||||
# ─ trash ─
|
||||
- { kind: trash, class_name: PRCXI_trash }
|
||||
models:
|
||||
# PRCXI 9320 —— 与厂商级完全一致(空 dict 仅作为合法 model 名占位)。
|
||||
# caller `--target_model 9320` 时所有字段继承厂商级 prcxi 段。
|
||||
"9320": {}
|
||||
# 演示:假想 PRCXI 4040 把 slot 4 物理位换到 16、trash 槽换到 20。
|
||||
# 仅 slot_remap 不同;rules 与厂商级一致 → 不重复声明(自动继承)。
|
||||
"4040":
|
||||
slot_remap:
|
||||
default:
|
||||
"4": "16"
|
||||
"8": "14"
|
||||
by_object:
|
||||
trash:
|
||||
"12": "20"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# 演示:未来加新仪器只复制 prcxi 段、改 device 名 + slot_remap + rules。
|
||||
# 特别注意 tip 量程档可与 PRCXI 不同。
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# beckman:
|
||||
# slot_remap:
|
||||
# default: {"4": "13"}
|
||||
# by_object: {trash: {"12": "16"}}
|
||||
# rules:
|
||||
# - { kind: tip_rack, hole_count: 96, volume_max: 20, class_name: Beckman_20uL_Tips }
|
||||
# - { kind: tip_rack, hole_count: 96, volume_max: 199.9, class_name: Beckman_200uL_Tips }
|
||||
# - { kind: tip_rack, hole_count: 96, class_name: Beckman_1000uL_Tips }
|
||||
# - { kind: tube_rack, hole_count: 24, class_name: Beckman_24_TubeRack }
|
||||
# - { kind: plate, hole_count: 96, class_name: Beckman_BioMek_96_wellplate }
|
||||
# - { kind: trash, class_name: Beckman_Trash }
|
||||
# models:
|
||||
# "i7":
|
||||
# slot_remap:
|
||||
# default: {"4": "13", "5": "14"} # 假想 i7 多一个 slot 重映射
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: ros-humble-unilabos-msgs
|
||||
version: 0.11.2
|
||||
version: 0.11.1
|
||||
source:
|
||||
path: ../../unilabos_msgs
|
||||
target_directory: src
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package:
|
||||
name: unilabos
|
||||
version: "0.11.2"
|
||||
version: "0.11.1"
|
||||
|
||||
source:
|
||||
path: ../..
|
||||
|
||||
2
setup.py
2
setup.py
@@ -4,7 +4,7 @@ package_name = 'unilabos'
|
||||
|
||||
setup(
|
||||
name=package_name,
|
||||
version='0.11.2',
|
||||
version='0.11.1',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=['setuptools'],
|
||||
|
||||
539
test/devices/test_prcxi.py
Normal file
539
test/devices/test_prcxi.py
Normal file
@@ -0,0 +1,539 @@
|
||||
import pytest
|
||||
import json
|
||||
import os
|
||||
import asyncio
|
||||
import collections
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from pylabrobot.resources import Coordinate
|
||||
from pylabrobot.resources.opentrons.tip_racks import opentrons_96_tiprack_300ul, opentrons_96_tiprack_10ul
|
||||
from pylabrobot.resources.opentrons.plates import corning_96_wellplate_360ul_flat, nest_96_wellplate_2ml_deep
|
||||
|
||||
from unilabos.devices.liquid_handling.prcxi.prcxi import (
|
||||
PRCXI9300Deck,
|
||||
PRCXI9300Container,
|
||||
PRCXI9300Trash,
|
||||
PRCXI9300Handler,
|
||||
PRCXI9300Backend,
|
||||
DefaultLayout,
|
||||
Material,
|
||||
WorkTablets,
|
||||
MatrixInfo
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def prcxi_materials() -> Dict[str, Any]:
|
||||
"""加载 PRCXI 物料数据"""
|
||||
print("加载 PRCXI 物料数据...")
|
||||
material_path = os.path.join(os.path.dirname(__file__), "..", "..", "unilabos", "devices", "liquid_handling", "prcxi", "prcxi_material.json")
|
||||
with open(material_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
print(f"加载了 {len(data)} 条物料数据")
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def prcxi_9300_deck() -> PRCXI9300Deck:
|
||||
"""创建 PRCXI 9300 工作台"""
|
||||
return PRCXI9300Deck(name="PRCXI_Deck_9300", size_x=100, size_y=100, size_z=100, model="9300")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def prcxi_9320_deck() -> PRCXI9300Deck:
|
||||
"""创建 PRCXI 9320 工作台"""
|
||||
return PRCXI9300Deck(name="PRCXI_Deck_9320", size_x=100, size_y=100, size_z=100, model="9320")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def prcxi_9300_handler(prcxi_9300_deck) -> PRCXI9300Handler:
|
||||
"""创建 PRCXI 9300 处理器(模拟模式)"""
|
||||
return PRCXI9300Handler(
|
||||
deck=prcxi_9300_deck,
|
||||
host="192.168.1.201",
|
||||
port=9999,
|
||||
timeout=10.0,
|
||||
channel_num=8,
|
||||
axis="Left",
|
||||
setup=False,
|
||||
debug=True,
|
||||
simulator=True,
|
||||
matrix_id="test-matrix-9300"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def prcxi_9320_handler(prcxi_9320_deck) -> PRCXI9300Handler:
|
||||
"""创建 PRCXI 9320 处理器(模拟模式)"""
|
||||
return PRCXI9300Handler(
|
||||
deck=prcxi_9320_deck,
|
||||
host="192.168.1.201",
|
||||
port=9999,
|
||||
timeout=10.0,
|
||||
channel_num=1,
|
||||
axis="Right",
|
||||
setup=False,
|
||||
debug=True,
|
||||
simulator=True,
|
||||
matrix_id="test-matrix-9320",
|
||||
is_9320=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tip_rack_300ul(prcxi_materials) -> PRCXI9300Container:
|
||||
"""创建 300μL 枪头盒"""
|
||||
tip_rack = PRCXI9300Container(
|
||||
name="tip_rack_300ul",
|
||||
size_x=50,
|
||||
size_y=50,
|
||||
size_z=10,
|
||||
category="tip_rack",
|
||||
ordering=collections.OrderedDict()
|
||||
)
|
||||
tip_rack.load_state({
|
||||
"Material": {
|
||||
"uuid": prcxi_materials["300μL Tip头"]["uuid"],
|
||||
"Code": "ZX-001-300",
|
||||
"Name": "300μL Tip头"
|
||||
}
|
||||
})
|
||||
return tip_rack
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tip_rack_10ul(prcxi_materials) -> PRCXI9300Container:
|
||||
"""创建 10μL 枪头盒"""
|
||||
tip_rack = PRCXI9300Container(
|
||||
name="tip_rack_10ul",
|
||||
size_x=50,
|
||||
size_y=50,
|
||||
size_z=10,
|
||||
category="tip_rack",
|
||||
ordering=collections.OrderedDict()
|
||||
)
|
||||
tip_rack.load_state({
|
||||
"Material": {
|
||||
"uuid": prcxi_materials["10μL加长 Tip头"]["uuid"],
|
||||
"Code": "ZX-001-10+",
|
||||
"Name": "10μL加长 Tip头"
|
||||
}
|
||||
})
|
||||
return tip_rack
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def well_plate_96(prcxi_materials) -> PRCXI9300Container:
|
||||
"""创建 96 孔板"""
|
||||
plate = PRCXI9300Container(
|
||||
name="well_plate_96",
|
||||
size_x=50,
|
||||
size_y=50,
|
||||
size_z=10,
|
||||
category="plate",
|
||||
ordering=collections.OrderedDict()
|
||||
)
|
||||
plate.load_state({
|
||||
"Material": {
|
||||
"uuid": prcxi_materials["96深孔板"]["uuid"],
|
||||
"Code": "ZX-019-2.2",
|
||||
"Name": "96深孔板"
|
||||
}
|
||||
})
|
||||
return plate
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def deep_well_plate(prcxi_materials) -> PRCXI9300Container:
|
||||
"""创建深孔板"""
|
||||
plate = PRCXI9300Container(
|
||||
name="deep_well_plate",
|
||||
size_x=50,
|
||||
size_y=50,
|
||||
size_z=10,
|
||||
category="plate",
|
||||
ordering=collections.OrderedDict()
|
||||
)
|
||||
plate.load_state({
|
||||
"Material": {
|
||||
"uuid": prcxi_materials["96深孔板"]["uuid"],
|
||||
"Code": "ZX-019-2.2",
|
||||
"Name": "96深孔板"
|
||||
}
|
||||
})
|
||||
return plate
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def trash_container(prcxi_materials) -> PRCXI9300Trash:
|
||||
"""创建垃圾桶"""
|
||||
trash = PRCXI9300Trash(name="trash", size_x=50, size_y=50, size_z=10, category="trash")
|
||||
trash.load_state({
|
||||
"Material": {
|
||||
"uuid": prcxi_materials["废弃槽"]["uuid"]
|
||||
}
|
||||
})
|
||||
return trash
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def default_layout_9300() -> DefaultLayout:
|
||||
"""创建 PRCXI 9300 默认布局"""
|
||||
return DefaultLayout("PRCXI9300")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def default_layout_9320() -> DefaultLayout:
|
||||
"""创建 PRCXI 9320 默认布局"""
|
||||
return DefaultLayout("PRCXI9320")
|
||||
|
||||
|
||||
class TestPRCXIDeckSetup:
|
||||
"""测试 PRCXI 工作台设置功能"""
|
||||
|
||||
def test_prcxi_9300_deck_creation(self, prcxi_9300_deck):
|
||||
"""测试 PRCXI 9300 工作台创建"""
|
||||
assert prcxi_9300_deck.name == "PRCXI_Deck_9300"
|
||||
assert len(prcxi_9300_deck.sites) == 6
|
||||
assert prcxi_9300_deck._size_x == 100
|
||||
assert prcxi_9300_deck._size_y == 100
|
||||
assert prcxi_9300_deck._size_z == 100
|
||||
|
||||
def test_prcxi_9320_deck_creation(self, prcxi_9320_deck):
|
||||
"""测试 PRCXI 9320 工作台创建"""
|
||||
assert prcxi_9320_deck.name == "PRCXI_Deck_9320"
|
||||
assert len(prcxi_9320_deck.sites) == 16
|
||||
assert prcxi_9320_deck._size_x == 100
|
||||
assert prcxi_9320_deck._size_y == 100
|
||||
assert prcxi_9320_deck._size_z == 100
|
||||
|
||||
def test_container_assignment(self, prcxi_9300_deck, tip_rack_300ul, well_plate_96, trash_container):
|
||||
"""测试容器分配到工作台"""
|
||||
# 分配枪头盒
|
||||
prcxi_9300_deck.assign_child_resource(tip_rack_300ul, location=Coordinate(0, 0, 0))
|
||||
assert tip_rack_300ul in prcxi_9300_deck.children
|
||||
|
||||
# 分配孔板
|
||||
prcxi_9300_deck.assign_child_resource(well_plate_96, location=Coordinate(0, 0, 0))
|
||||
assert well_plate_96 in prcxi_9300_deck.children
|
||||
|
||||
# 分配垃圾桶
|
||||
prcxi_9300_deck.assign_child_resource(trash_container, location=Coordinate(0, 0, 0))
|
||||
assert trash_container in prcxi_9300_deck.children
|
||||
|
||||
def test_container_material_loading(self, tip_rack_300ul, well_plate_96, prcxi_materials):
|
||||
"""测试容器物料信息加载"""
|
||||
# 测试枪头盒物料信息
|
||||
tip_material = tip_rack_300ul._unilabos_state["Material"]
|
||||
assert tip_material["uuid"] == prcxi_materials["300μL Tip头"]["uuid"]
|
||||
assert tip_material["Name"] == "300μL Tip头"
|
||||
|
||||
# 测试孔板物料信息
|
||||
plate_material = well_plate_96._unilabos_state["Material"]
|
||||
assert plate_material["uuid"] == prcxi_materials["96深孔板"]["uuid"]
|
||||
assert plate_material["Name"] == "96深孔板"
|
||||
|
||||
|
||||
class TestPRCXISingleStepOperations:
|
||||
"""测试 PRCXI 单步操作功能"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pick_up_tips_single_channel(self, prcxi_9320_handler, prcxi_9320_deck, tip_rack_10ul):
|
||||
"""测试单通道拾取枪头"""
|
||||
# 将枪头盒添加到工作台
|
||||
prcxi_9320_deck.assign_child_resource(tip_rack_10ul, location=Coordinate(0, 0, 0))
|
||||
|
||||
# 初始化处理器
|
||||
await prcxi_9320_handler.setup()
|
||||
|
||||
# 设置枪头盒
|
||||
prcxi_9320_handler.set_tiprack([tip_rack_10ul])
|
||||
|
||||
# 创建模拟的枪头位置
|
||||
from pylabrobot.resources import TipSpot, Tip
|
||||
tip = Tip(has_filter=False, total_tip_length=10, maximal_volume=10, fitting_depth=5)
|
||||
tip_spot = TipSpot("A1", size_x=1, size_y=1, size_z=1, make_tip=lambda: tip)
|
||||
tip_rack_10ul.assign_child_resource(tip_spot, location=Coordinate(0, 0, 0))
|
||||
|
||||
# 直接测试后端方法
|
||||
from pylabrobot.liquid_handling import Pickup
|
||||
pickup = Pickup(resource=tip_spot, offset=None, tip=tip)
|
||||
await prcxi_9320_handler._unilabos_backend.pick_up_tips([pickup], [0])
|
||||
|
||||
# 验证步骤已添加到待办列表
|
||||
assert len(prcxi_9320_handler._unilabos_backend.steps_todo_list) == 1
|
||||
step = prcxi_9320_handler._unilabos_backend.steps_todo_list[0]
|
||||
assert step["Function"] == "Load"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pick_up_tips_multi_channel(self, prcxi_9300_handler, tip_rack_300ul):
|
||||
"""测试多通道拾取枪头"""
|
||||
# 设置枪头盒
|
||||
prcxi_9300_handler.set_tiprack([tip_rack_300ul])
|
||||
|
||||
# 拾取8个枪头
|
||||
tip_spots = tip_rack_300ul.children[:8]
|
||||
await prcxi_9300_handler.pick_up_tips(tip_spots, [0, 1, 2, 3, 4, 5, 6, 7])
|
||||
|
||||
# 验证步骤已添加到待办列表
|
||||
assert len(prcxi_9300_handler._unilabos_backend.steps_todo_list) == 1
|
||||
step = prcxi_9300_handler._unilabos_backend.steps_todo_list[0]
|
||||
assert step["Function"] == "Load"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aspirate_single_channel(self, prcxi_9320_handler, well_plate_96):
|
||||
"""测试单通道吸取液体"""
|
||||
# 设置液体
|
||||
well = well_plate_96.get_item("A1")
|
||||
prcxi_9320_handler.set_liquid([well], ["water"], [50])
|
||||
|
||||
# 吸取液体
|
||||
await prcxi_9320_handler.aspirate([well], [50], [0])
|
||||
|
||||
# 验证步骤已添加到待办列表
|
||||
assert len(prcxi_9320_handler._unilabos_backend.steps_todo_list) == 1
|
||||
step = prcxi_9320_handler._unilabos_backend.steps_todo_list[0]
|
||||
assert step["Function"] == "Imbibing"
|
||||
assert step["DosageNum"] == 50
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dispense_single_channel(self, prcxi_9320_handler, well_plate_96):
|
||||
"""测试单通道分配液体"""
|
||||
# 分配液体
|
||||
well = well_plate_96.get_item("A1")
|
||||
await prcxi_9320_handler.dispense([well], [25], [0])
|
||||
|
||||
# 验证步骤已添加到待办列表
|
||||
assert len(prcxi_9320_handler._unilabos_backend.steps_todo_list) == 1
|
||||
step = prcxi_9320_handler._unilabos_backend.steps_todo_list[0]
|
||||
assert step["Function"] == "Tapping"
|
||||
assert step["DosageNum"] == 25
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mix_single_channel(self, prcxi_9320_handler, well_plate_96):
|
||||
"""测试单通道混合液体"""
|
||||
# 混合液体
|
||||
well = well_plate_96.get_item("A1")
|
||||
await prcxi_9320_handler.mix([well], mix_time=3, mix_vol=50)
|
||||
|
||||
# 验证步骤已添加到待办列表
|
||||
assert len(prcxi_9320_handler._unilabos_backend.steps_todo_list) == 1
|
||||
step = prcxi_9320_handler._unilabos_backend.steps_todo_list[0]
|
||||
assert step["Function"] == "Blending"
|
||||
assert step["BlendingTimes"] == 3
|
||||
assert step["DosageNum"] == 50
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_drop_tips_to_trash(self, prcxi_9320_handler, trash_container):
|
||||
"""测试丢弃枪头到垃圾桶"""
|
||||
# 丢弃枪头
|
||||
await prcxi_9320_handler.drop_tips([trash_container], [0])
|
||||
|
||||
# 验证步骤已添加到待办列表
|
||||
assert len(prcxi_9320_handler._unilabos_backend.steps_todo_list) == 1
|
||||
step = prcxi_9320_handler._unilabos_backend.steps_todo_list[0]
|
||||
assert step["Function"] == "UnLoad"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_discard_tips(self, prcxi_9320_handler):
|
||||
"""测试丢弃枪头"""
|
||||
# 丢弃枪头
|
||||
await prcxi_9320_handler.discard_tips([0])
|
||||
|
||||
# 验证步骤已添加到待办列表
|
||||
assert len(prcxi_9320_handler._unilabos_backend.steps_todo_list) == 1
|
||||
step = prcxi_9320_handler._unilabos_backend.steps_todo_list[0]
|
||||
assert step["Function"] == "UnLoad"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_liquid_transfer_workflow(self, prcxi_9320_handler, tip_rack_10ul, well_plate_96):
|
||||
"""测试完整的液体转移工作流程"""
|
||||
# 设置枪头盒和液体
|
||||
prcxi_9320_handler.set_tiprack([tip_rack_10ul])
|
||||
source_well = well_plate_96.get_item("A1")
|
||||
target_well = well_plate_96.get_item("B1")
|
||||
prcxi_9320_handler.set_liquid([source_well], ["water"], [100])
|
||||
|
||||
# 创建协议
|
||||
await prcxi_9320_handler.create_protocol(protocol_name="Test Transfer Protocol")
|
||||
|
||||
# 执行转移流程
|
||||
tip_spot = tip_rack_10ul.get_item("A1")
|
||||
await prcxi_9320_handler.pick_up_tips([tip_spot], [0])
|
||||
await prcxi_9320_handler.aspirate([source_well], [50], [0])
|
||||
await prcxi_9320_handler.dispense([target_well], [50], [0])
|
||||
await prcxi_9320_handler.discard_tips([0])
|
||||
|
||||
# 验证所有步骤都已添加
|
||||
assert len(prcxi_9320_handler._unilabos_backend.steps_todo_list) == 4
|
||||
functions = [step["Function"] for step in prcxi_9320_handler._unilabos_backend.steps_todo_list]
|
||||
assert functions == ["Load", "Imbibing", "Tapping", "UnLoad"]
|
||||
|
||||
|
||||
class TestPRCXILayoutRecommendation:
|
||||
"""测试 PRCXI 板位推荐功能"""
|
||||
|
||||
def test_9300_layout_creation(self, default_layout_9300):
|
||||
"""测试 PRCXI 9300 布局创建"""
|
||||
layout_info = default_layout_9300.get_layout()
|
||||
assert layout_info["rows"] == 2
|
||||
assert layout_info["columns"] == 3
|
||||
assert len(layout_info["layout"]) == 6
|
||||
assert layout_info["trash_slot"] == 6
|
||||
assert "waste_liquid_slot" not in layout_info
|
||||
|
||||
def test_9320_layout_creation(self, default_layout_9320):
|
||||
"""测试 PRCXI 9320 布局创建"""
|
||||
layout_info = default_layout_9320.get_layout()
|
||||
assert layout_info["rows"] == 4
|
||||
assert layout_info["columns"] == 4
|
||||
assert len(layout_info["layout"]) == 16
|
||||
assert layout_info["trash_slot"] == 16
|
||||
assert layout_info["waste_liquid_slot"] == 12
|
||||
|
||||
def test_layout_recommendation_9320(self, default_layout_9320, prcxi_materials):
|
||||
"""测试 PRCXI 9320 板位推荐功能"""
|
||||
# 添加物料信息
|
||||
default_layout_9320.add_lab_resource(prcxi_materials)
|
||||
|
||||
# 推荐布局
|
||||
needs = [
|
||||
("reagent_1", "96 细胞培养皿", 3),
|
||||
("reagent_2", "12道储液槽", 1),
|
||||
("reagent_3", "200μL Tip头", 7),
|
||||
("reagent_4", "10μL加长 Tip头", 1),
|
||||
]
|
||||
|
||||
matrix_layout, layout_list = default_layout_9320.recommend_layout(needs)
|
||||
|
||||
# 验证返回结果
|
||||
assert "MatrixId" in matrix_layout
|
||||
assert "MatrixName" in matrix_layout
|
||||
assert "MatrixCount" in matrix_layout
|
||||
assert "WorkTablets" in matrix_layout
|
||||
assert len(layout_list) == 12 # 3+1+7+1 = 12个位置
|
||||
|
||||
# 验证推荐的位置不包含预留位置
|
||||
reserved_positions = {12, 16}
|
||||
recommended_positions = [item["positions"] for item in layout_list]
|
||||
for pos in recommended_positions:
|
||||
assert pos not in reserved_positions
|
||||
|
||||
def test_layout_recommendation_insufficient_space(self, default_layout_9320, prcxi_materials):
|
||||
"""测试板位推荐空间不足的情况"""
|
||||
# 添加物料信息
|
||||
default_layout_9320.add_lab_resource(prcxi_materials)
|
||||
|
||||
# 尝试推荐超过可用空间的布局
|
||||
needs = [
|
||||
("reagent_1", "96 细胞培养皿", 15), # 需要15个位置,但只有14个可用
|
||||
]
|
||||
|
||||
with pytest.raises(ValueError, match="需要 .* 个位置,但只有 .* 个可用位置"):
|
||||
default_layout_9320.recommend_layout(needs)
|
||||
|
||||
def test_layout_recommendation_material_not_found(self, default_layout_9320, prcxi_materials):
|
||||
"""测试板位推荐物料不存在的情况"""
|
||||
# 添加物料信息
|
||||
default_layout_9320.add_lab_resource(prcxi_materials)
|
||||
|
||||
# 尝试推荐不存在的物料
|
||||
needs = [
|
||||
("reagent_1", "不存在的物料", 1),
|
||||
]
|
||||
|
||||
with pytest.raises(ValueError, match="Material .* not found in lab resources"):
|
||||
default_layout_9320.recommend_layout(needs)
|
||||
|
||||
|
||||
class TestPRCXIBackendOperations:
|
||||
"""测试 PRCXI 后端操作功能"""
|
||||
|
||||
def test_backend_initialization(self, prcxi_9300_handler):
|
||||
"""测试后端初始化"""
|
||||
backend = prcxi_9300_handler._unilabos_backend
|
||||
assert isinstance(backend, PRCXI9300Backend)
|
||||
assert backend._num_channels == 8
|
||||
assert backend.debug is True
|
||||
|
||||
def test_protocol_creation(self, prcxi_9300_handler):
|
||||
"""测试协议创建"""
|
||||
backend = prcxi_9300_handler._unilabos_backend
|
||||
backend.create_protocol("Test Protocol")
|
||||
assert backend.protocol_name == "Test Protocol"
|
||||
assert len(backend.steps_todo_list) == 0
|
||||
|
||||
def test_channel_validation(self):
|
||||
"""测试通道验证"""
|
||||
# 测试正确的8通道配置
|
||||
valid_channels = [0, 1, 2, 3, 4, 5, 6, 7]
|
||||
result = PRCXI9300Backend.check_channels(valid_channels)
|
||||
assert result == valid_channels
|
||||
|
||||
# 测试错误的通道配置
|
||||
invalid_channels = [0, 1, 2, 3]
|
||||
result = PRCXI9300Backend.check_channels(invalid_channels)
|
||||
assert result == [0, 1, 2, 3, 4, 5, 6, 7]
|
||||
|
||||
def test_matrix_info_creation(self, prcxi_9300_handler):
|
||||
"""测试矩阵信息创建"""
|
||||
backend = prcxi_9300_handler._unilabos_backend
|
||||
backend.create_protocol("Test Protocol")
|
||||
|
||||
# 模拟运行协议时的矩阵信息创建
|
||||
run_time = 1234567890
|
||||
matrix_info = MatrixInfo(
|
||||
MatrixId=f"{int(run_time)}",
|
||||
MatrixName=f"protocol_{run_time}",
|
||||
MatrixCount=len(backend.tablets_info),
|
||||
WorkTablets=backend.tablets_info,
|
||||
)
|
||||
|
||||
assert matrix_info["MatrixId"] == str(int(run_time))
|
||||
assert matrix_info["MatrixName"] == f"protocol_{run_time}"
|
||||
assert "WorkTablets" in matrix_info
|
||||
|
||||
|
||||
class TestPRCXIContainerOperations:
|
||||
"""测试 PRCXI 容器操作功能"""
|
||||
|
||||
def test_container_serialization(self, tip_rack_300ul):
|
||||
"""测试容器序列化"""
|
||||
serialized = tip_rack_300ul.serialize_state()
|
||||
assert "Material" in serialized
|
||||
assert serialized["Material"]["Name"] == "300μL Tip头"
|
||||
|
||||
def test_container_deserialization(self, tip_rack_300ul):
|
||||
"""测试容器反序列化"""
|
||||
# 序列化
|
||||
serialized = tip_rack_300ul.serialize_state()
|
||||
|
||||
# 创建新容器并反序列化
|
||||
new_tip_rack = PRCXI9300Container(
|
||||
name="new_tip_rack",
|
||||
size_x=50,
|
||||
size_y=50,
|
||||
size_z=10,
|
||||
category="tip_rack",
|
||||
ordering=collections.OrderedDict()
|
||||
)
|
||||
new_tip_rack.load_state(serialized)
|
||||
|
||||
assert new_tip_rack._unilabos_state["Material"]["Name"] == "300μL Tip头"
|
||||
|
||||
def test_trash_container_creation(self, prcxi_materials):
|
||||
"""测试垃圾桶容器创建"""
|
||||
trash = PRCXI9300Trash(name="trash", size_x=50, size_y=50, size_z=10, category="trash")
|
||||
trash.load_state({
|
||||
"Material": {
|
||||
"uuid": prcxi_materials["废弃槽"]["uuid"]
|
||||
}
|
||||
})
|
||||
|
||||
assert trash.name == "trash"
|
||||
assert trash._unilabos_state["Material"]["uuid"] == prcxi_materials["废弃槽"]["uuid"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 运行测试
|
||||
pytest.main([__file__, "-v"])
|
||||
15
tests/devices/liquid_handling/README.md
Normal file
15
tests/devices/liquid_handling/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Liquid handling 集成测试
|
||||
|
||||
`test_transfer_liquid.py` 现在会调用 PRCXI 的 RViz 仿真 backend,运行前请确保:
|
||||
|
||||
1. 已安装包含 `pylabrobot`、`rclpy` 的运行环境;
|
||||
2. 启动 ROS 依赖(`rviz` 可选,但是 `rviz_backend` 会创建 ROS 节点);
|
||||
3. 在 shell 中设置 `UNILAB_SIM_TEST=1`,否则 pytest 会自动跳过这些慢速用例:
|
||||
|
||||
```bash
|
||||
export UNILAB_SIM_TEST=1
|
||||
pytest tests/devices/liquid_handling/test_transfer_liquid.py -m slow
|
||||
```
|
||||
|
||||
如果只需验证逻辑层(不依赖仿真),可以直接运行 `tests/devices/liquid_handling/unit_test.py`,该文件使用 Fake backend,适合作为 CI 的快速测试。***
|
||||
|
||||
244
tests/devices/liquid_handling/test_liquid_history.py
Normal file
244
tests/devices/liquid_handling/test_liquid_history.py
Normal file
@@ -0,0 +1,244 @@
|
||||
"""P9 — ``liquid_history`` schema v3 + helper 单元测试。
|
||||
|
||||
测试覆盖:
|
||||
- :func:`append_liquid_history`:写 v3 entry / tracker 缺失 graceful / 滚动上限
|
||||
- :func:`normalize_liquid_history`:v3 dict / v2 tuple / list[str] / 混合 / 非法
|
||||
- :func:`well_current_liquid_name`:tracker.liquids 末项 / get_liquids fallback / 缺失
|
||||
|
||||
注:``LiquidHandlerAbstract.set_liquid`` 写 history 的集成("set" action)覆盖
|
||||
逻辑相同(直接调用 :func:`append_liquid_history`),由本测试间接验证;端到端走 PLR
|
||||
真实 ``Well.set_liquids`` 的集成测试在 ``tests/devices/liquid_handling/unit_test.py``
|
||||
范围内随 PLR 环境就绪后增补,本 P9 提交保持解耦。
|
||||
|
||||
详见 ``product_designs/protocol_convert/09-liquid-history-unknown-debug.md`` §8。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
# liquid_history 模块**不依赖** pylabrobot,可在 PLR 环境缺失时独立 import / 单测。
|
||||
from unilabos.devices.liquid_handling.liquid_history import (
|
||||
LIQUID_HISTORY_MAX_ENTRIES,
|
||||
LiquidHistoryEntry,
|
||||
append_liquid_history,
|
||||
normalize_liquid_history,
|
||||
well_current_liquid_name,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures:DummyTracker / DummyWell(避免引入真实 PLR Well/VolumeTracker 依赖)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class DummyTracker:
|
||||
"""模拟 PLR VolumeTracker:仅暴露 P9 hook 关心的字段。"""
|
||||
|
||||
liquid_history: List[Any] = field(default_factory=list)
|
||||
liquids: List[Tuple[Any, float]] = field(default_factory=list)
|
||||
max_volume: float = 200.0
|
||||
is_disabled: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class DummyWell:
|
||||
"""模拟 PLR Well:仅暴露 ``tracker``。"""
|
||||
|
||||
name: str = "well_A1"
|
||||
max_volume: float = 200.0
|
||||
tracker: DummyTracker = field(default_factory=DummyTracker)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# append_liquid_history
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAppendLiquidHistory:
|
||||
def test_append_creates_v3_entry(self) -> None:
|
||||
well = DummyWell()
|
||||
append_liquid_history(well, "Plasma", 100.0, "set")
|
||||
|
||||
assert len(well.tracker.liquid_history) == 1
|
||||
entry = well.tracker.liquid_history[0]
|
||||
assert entry["name"] == "Plasma"
|
||||
assert entry["volume"] == 100.0
|
||||
assert entry["action"] == "set"
|
||||
assert "timestamp" in entry and isinstance(entry["timestamp"], str)
|
||||
|
||||
def test_append_aspirate_negative_volume(self) -> None:
|
||||
well = DummyWell()
|
||||
append_liquid_history(well, "Water", -50.0, "aspirate")
|
||||
|
||||
assert well.tracker.liquid_history[0]["volume"] == -50.0
|
||||
assert well.tracker.liquid_history[0]["action"] == "aspirate"
|
||||
|
||||
def test_append_with_empty_name_keeps_empty_string(self) -> None:
|
||||
"""name 为空时应写入 ``""`` 而非字面 "unknown"(避免视觉混淆 bottom_type)。"""
|
||||
well = DummyWell()
|
||||
append_liquid_history(well, "", 50.0, "dispense")
|
||||
|
||||
assert well.tracker.liquid_history[0]["name"] == ""
|
||||
|
||||
def test_append_with_none_name_normalized_to_empty_string(self) -> None:
|
||||
well = DummyWell()
|
||||
append_liquid_history(well, None, 50.0, "dispense") # type: ignore[arg-type]
|
||||
|
||||
assert well.tracker.liquid_history[0]["name"] == ""
|
||||
|
||||
def test_append_initializes_history_if_missing(self) -> None:
|
||||
"""tracker 没有 liquid_history 属性时 helper 自动创建空 list 并写入。"""
|
||||
well = DummyWell()
|
||||
del well.tracker.liquid_history # 模拟全新 PLR tracker
|
||||
append_liquid_history(well, "X", 10.0, "set")
|
||||
|
||||
assert hasattr(well.tracker, "liquid_history")
|
||||
assert len(well.tracker.liquid_history) == 1
|
||||
|
||||
def test_append_no_tracker_is_graceful(self) -> None:
|
||||
"""well 无 tracker 时静默不抛(保护主流程)。"""
|
||||
|
||||
class NoTrackerWell:
|
||||
name = "no_tracker"
|
||||
|
||||
well = NoTrackerWell()
|
||||
append_liquid_history(well, "X", 10.0, "set") # 不应抛
|
||||
assert not hasattr(well, "tracker")
|
||||
|
||||
def test_append_action_defaults_to_legacy_when_empty(self) -> None:
|
||||
well = DummyWell()
|
||||
append_liquid_history(well, "X", 1.0, "")
|
||||
|
||||
assert well.tracker.liquid_history[0]["action"] == "legacy"
|
||||
|
||||
def test_append_respects_max_entries_rolling(self) -> None:
|
||||
"""超过 ``LIQUID_HISTORY_MAX_ENTRIES`` 时丢弃头部,保留最近 entries。"""
|
||||
well = DummyWell()
|
||||
well.tracker.liquid_history = [
|
||||
{"name": f"old_{i}"} for i in range(LIQUID_HISTORY_MAX_ENTRIES + 5)
|
||||
]
|
||||
append_liquid_history(well, "newest", 1.0, "set")
|
||||
|
||||
assert len(well.tracker.liquid_history) == LIQUID_HISTORY_MAX_ENTRIES
|
||||
assert well.tracker.liquid_history[-1]["name"] == "newest"
|
||||
assert well.tracker.liquid_history[0]["name"] != "old_0"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# normalize_liquid_history
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestNormalizeLiquidHistory:
|
||||
def test_v3_dict_passthrough_with_field_defaults(self) -> None:
|
||||
raw = [{"name": "A", "volume": 100, "action": "set", "timestamp": "2026-05-22T00:00:00Z"}]
|
||||
result = normalize_liquid_history(raw)
|
||||
|
||||
assert result == [{
|
||||
"name": "A",
|
||||
"volume": 100.0,
|
||||
"action": "set",
|
||||
"timestamp": "2026-05-22T00:00:00Z",
|
||||
}]
|
||||
|
||||
def test_v3_dict_missing_optional_fields_filled_with_defaults(self) -> None:
|
||||
raw = [{"name": "A"}]
|
||||
result = normalize_liquid_history(raw)
|
||||
|
||||
assert result == [{"name": "A", "volume": 0.0, "action": "legacy"}]
|
||||
assert "timestamp" not in result[0]
|
||||
|
||||
def test_v2_tuple_upgraded_to_v3_legacy(self) -> None:
|
||||
raw = [("A", 100), ("B", 50.5)]
|
||||
result = normalize_liquid_history(raw)
|
||||
|
||||
assert result == [
|
||||
{"name": "A", "volume": 100.0, "action": "legacy"},
|
||||
{"name": "B", "volume": 50.5, "action": "legacy"},
|
||||
]
|
||||
|
||||
def test_list_of_strings_upgraded(self) -> None:
|
||||
raw = ["A", "B"]
|
||||
result = normalize_liquid_history(raw)
|
||||
|
||||
assert result == [
|
||||
{"name": "A", "volume": 0.0, "action": "legacy"},
|
||||
{"name": "B", "volume": 0.0, "action": "legacy"},
|
||||
]
|
||||
|
||||
def test_mixed_input_normalized(self) -> None:
|
||||
raw = [
|
||||
{"name": "A", "volume": 1, "action": "set"},
|
||||
("B", 2),
|
||||
"C",
|
||||
]
|
||||
result = normalize_liquid_history(raw)
|
||||
|
||||
assert [e["name"] for e in result] == ["A", "B", "C"]
|
||||
assert [e["action"] for e in result] == ["set", "legacy", "legacy"]
|
||||
|
||||
def test_invalid_entries_dropped(self) -> None:
|
||||
raw = [42, None, {"name": "A"}, ("only_one",)]
|
||||
result = normalize_liquid_history(raw)
|
||||
|
||||
# 只保留 {"name": "A"} 这一条;其它都被丢弃
|
||||
assert len(result) == 1
|
||||
assert result[0]["name"] == "A"
|
||||
assert result[0]["volume"] == 0.0 # 缺省补 0
|
||||
|
||||
def test_non_list_input_returns_empty(self) -> None:
|
||||
assert normalize_liquid_history(None) == []
|
||||
assert normalize_liquid_history("not_a_list") == []
|
||||
assert normalize_liquid_history({"name": "X"}) == []
|
||||
|
||||
def test_tuple_with_unconvertible_volume_falls_back_to_zero(self) -> None:
|
||||
raw = [("A", "not_a_number")]
|
||||
result = normalize_liquid_history(raw)
|
||||
|
||||
assert result[0]["volume"] == 0.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# well_current_liquid_name
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestWellCurrentLiquidName:
|
||||
def test_returns_last_liquid_name_from_tuple(self) -> None:
|
||||
well = DummyWell()
|
||||
well.tracker.liquids = [("Water", 50.0), ("Plasma", 100.0)]
|
||||
assert well_current_liquid_name(well) == "Plasma"
|
||||
|
||||
def test_returns_enum_like_name_attr(self) -> None:
|
||||
class FakeLiquid:
|
||||
name = "ETHANOL"
|
||||
|
||||
well = DummyWell()
|
||||
well.tracker.liquids = [(FakeLiquid(), 100.0)]
|
||||
assert well_current_liquid_name(well) == "ETHANOL"
|
||||
|
||||
def test_empty_liquids_returns_empty_string(self) -> None:
|
||||
well = DummyWell()
|
||||
well.tracker.liquids = []
|
||||
assert well_current_liquid_name(well) == ""
|
||||
|
||||
def test_no_tracker_returns_empty_string(self) -> None:
|
||||
class NoTrackerWell:
|
||||
name = "x"
|
||||
|
||||
assert well_current_liquid_name(NoTrackerWell()) == ""
|
||||
|
||||
def test_none_liquid_returns_empty_string(self) -> None:
|
||||
well = DummyWell()
|
||||
well.tracker.liquids = [(None, 100.0)]
|
||||
assert well_current_liquid_name(well) == ""
|
||||
|
||||
def test_string_liquid_returned_as_is(self) -> None:
|
||||
well = DummyWell()
|
||||
well.tracker.liquids = ["Saline"]
|
||||
assert well_current_liquid_name(well) == "Saline"
|
||||
@@ -0,0 +1,239 @@
|
||||
"""P2 v2 跨板能力验证 —— device 层 ``set_liquid_from_plate`` 单测。
|
||||
|
||||
对应 ``product_designs/protocol_convert/02-cross-slot-merge.md`` §9.1 / §9.5 step 6.3。
|
||||
|
||||
本测试聚焦于 **`_set_liquid_grouped_by_plate`** 已天然支持跨板 wells 的能力(v2 设计
|
||||
的核心依据):
|
||||
|
||||
- 输入 ``wells`` 列表来自多个 plate(每板各一/多个 well)时,``set_liquid`` 应按 plate
|
||||
分桶串行调用,每板一次(plate-bucket 顺序按 first-occurrence)。
|
||||
- 同板内多孔归到同一桶。
|
||||
- 返回 ``volumes`` 按 **输入 index 顺序**回拼,与 wells 一致 —— 这是 v2 Stage 3
|
||||
merged ``set_liquid_from_plate.output_wells`` 的顺序权威来源。
|
||||
- ``Well.set_liquids`` 在 ``set_liquid`` 链内被逐孔调用,与 PLR 实现的预期接口一致。
|
||||
|
||||
为了避免引入完整 PLR 资源树,测试用 duck-typed ``DummyWell`` / ``DummyPlate`` +
|
||||
``ResourceTreeSet`` 的 monkeypatch(dump 直接返回输入列表)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# 跨环境兼容:与现有 ``tests/devices/liquid_handling/test_transfer_liquid.py`` 一致,
|
||||
# 本测试通过 import ``unilabos.devices.liquid_handling.liquid_handler_abstract``
|
||||
# 拉起 pylabrobot 链;某些本地开发机的 pylabrobot 版本与代码库要求不一致,
|
||||
# 会在 import 阶段抛 ``ImportError``。这里用 ``importorskip`` 优雅跳过,让
|
||||
# CI(统一 pylabrobot 版本)跑全;纯逻辑测试(Stage 2 / Stage 3)不受影响。
|
||||
# ----------------------------------------------------------------------
|
||||
LiquidHandlerAbstract = pytest.importorskip(
|
||||
"unilabos.devices.liquid_handling.liquid_handler_abstract",
|
||||
reason="pylabrobot 链未完整可用,跳过 device 单测;CI 上请保证 pylabrobot ≥ 项目要求版本",
|
||||
exc_type=ImportError,
|
||||
).LiquidHandlerAbstract
|
||||
|
||||
|
||||
# ==================== Duck-typed PLR-like 资源 ====================
|
||||
|
||||
|
||||
@dataclass
|
||||
class DummyPlate:
|
||||
name: str
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover
|
||||
return f"DummyPlate({self.name})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DummyWell:
|
||||
name: str
|
||||
parent: DummyPlate
|
||||
max_volume: float = 1000.0
|
||||
liquid_history: List[Tuple[str, float]] = field(default_factory=list)
|
||||
|
||||
def set_liquids(self, items):
|
||||
"""模拟 PLR ``Well.set_liquids([(name, vol), ...])`` 接口。"""
|
||||
for name, vol in items:
|
||||
self.liquid_history.append((str(name), float(vol)))
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover
|
||||
return f"DummyWell({self.parent.name}/{self.name})"
|
||||
|
||||
|
||||
# ==================== fixture:装一台 FakeLiquidHandler ====================
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def patched_resource_tree(monkeypatch):
|
||||
"""patch ``ResourceTreeSet.from_plr_resources`` 使其接受 duck-typed wells/plates。
|
||||
|
||||
返回的对象只要带 ``.dump()`` 即可(``_set_liquid_grouped_by_plate`` 仅消费该方法)。
|
||||
"""
|
||||
from unilabos.devices.liquid_handling import liquid_handler_abstract as lha
|
||||
|
||||
class _FakeTree:
|
||||
def __init__(self, items):
|
||||
self._items = items
|
||||
|
||||
def dump(self):
|
||||
return [
|
||||
{"name": getattr(x, "name", None), "type": type(x).__name__}
|
||||
for x in self._items
|
||||
]
|
||||
|
||||
def _fake_from_plr_resources(items, known_newly_created=False): # noqa: ARG001
|
||||
return _FakeTree(list(items))
|
||||
|
||||
monkeypatch.setattr(
|
||||
lha.ResourceTreeSet,
|
||||
"from_plr_resources",
|
||||
staticmethod(_fake_from_plr_resources),
|
||||
)
|
||||
return lha
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def handler(patched_resource_tree):
|
||||
"""构造一台最小 LiquidHandlerAbstract 实例,绕过真实 backend / deck。"""
|
||||
|
||||
class _FakeHandler(LiquidHandlerAbstract):
|
||||
def __init__(self):
|
||||
# 不调用 super().__init__,避免真实硬件/后端依赖
|
||||
self.channel_num = 8
|
||||
self.support_touch_tip = True
|
||||
|
||||
return _FakeHandler()
|
||||
|
||||
|
||||
def _wells_grid(plate_name: str, well_names: List[str]) -> List[DummyWell]:
|
||||
plate = DummyPlate(name=plate_name)
|
||||
return [DummyWell(name=w, parent=plate) for w in well_names]
|
||||
|
||||
|
||||
# ==================== 用例 ====================
|
||||
|
||||
|
||||
def test_grouped_by_plate_single_plate_set_liquid_inline(handler):
|
||||
"""单 plate 多孔:set_liquids 按 wells 顺序逐项调用,volumes 回拼一致。"""
|
||||
wells = _wells_grid("plate_slot2", ["A1", "A2", "A3"])
|
||||
ret = handler._set_liquid_grouped_by_plate(
|
||||
wells=wells,
|
||||
liquid_names=["reagent_X"] * 3,
|
||||
volumes=[10.0, 20.0, 30.0],
|
||||
)
|
||||
|
||||
# 每个 well 的 liquid_history 各 1 条
|
||||
for w, expected_vol in zip(wells, [10.0, 20.0, 30.0]):
|
||||
assert w.liquid_history == [("reagent_X", expected_vol)]
|
||||
|
||||
# 返回 volumes 顺序与输入一致
|
||||
assert ret.volumes == [10.0, 20.0, 30.0]
|
||||
|
||||
|
||||
def test_grouped_by_plate_cross_plate_buckets_by_parent(handler):
|
||||
"""跨板 wells 列表 → 按 first-occurrence plate 顺序分桶,每板单独 set_liquid。
|
||||
|
||||
51b9a5 简化(每板 1 孔):4 plate × 1 well = 4 set_liquids 调用。
|
||||
"""
|
||||
p2 = _wells_grid("plate_slot2", ["A1"])
|
||||
p3 = _wells_grid("plate_slot3", ["A1"])
|
||||
p5 = _wells_grid("plate_slot5", ["A1"])
|
||||
p6 = _wells_grid("plate_slot6", ["A1"])
|
||||
wells = p2 + p3 + p5 + p6
|
||||
|
||||
ret = handler._set_liquid_grouped_by_plate(
|
||||
wells=wells,
|
||||
liquid_names=["l1"] * 4,
|
||||
volumes=[8.3] * 4,
|
||||
)
|
||||
|
||||
# 每个 well 都被 set_liquids 设过
|
||||
for w in wells:
|
||||
assert w.liquid_history == [("l1", 8.3)], f"well {w.parent.name}/{w.name} 未正确设液"
|
||||
|
||||
# volumes 顺序与输入对齐
|
||||
assert ret.volumes == [8.3, 8.3, 8.3, 8.3]
|
||||
|
||||
# plate dump 应含 4 个 plate(按 first-occurrence)
|
||||
plate_dump = ret.plate
|
||||
plate_names = [p["name"] for p in plate_dump]
|
||||
assert plate_names == ["plate_slot2", "plate_slot3", "plate_slot5", "plate_slot6"]
|
||||
|
||||
|
||||
def test_grouped_by_plate_interleaved_cross_plate_preserves_input_order(handler):
|
||||
"""交错跨板:wells=[p2.A1, p3.A1, p2.A2, p5.A1] → volumes 顺序按输入回拼。
|
||||
|
||||
内部仍按 plate 分桶执行 set_liquid(per-plate 串行),但返回顺序遵循输入 index。
|
||||
"""
|
||||
p2 = DummyPlate(name="plate_slot2")
|
||||
p3 = DummyPlate(name="plate_slot3")
|
||||
p5 = DummyPlate(name="plate_slot5")
|
||||
w_p2_a1 = DummyWell(name="A1", parent=p2)
|
||||
w_p2_a2 = DummyWell(name="A2", parent=p2)
|
||||
w_p3_a1 = DummyWell(name="A1", parent=p3)
|
||||
w_p5_a1 = DummyWell(name="A1", parent=p5)
|
||||
|
||||
wells = [w_p2_a1, w_p3_a1, w_p2_a2, w_p5_a1]
|
||||
ret = handler._set_liquid_grouped_by_plate(
|
||||
wells=wells,
|
||||
liquid_names=["l1"] * 4,
|
||||
volumes=[10.0, 20.0, 30.0, 40.0],
|
||||
)
|
||||
|
||||
# 每个 well 都被设液
|
||||
assert w_p2_a1.liquid_history == [("l1", 10.0)]
|
||||
assert w_p3_a1.liquid_history == [("l1", 20.0)]
|
||||
assert w_p2_a2.liquid_history == [("l1", 30.0)]
|
||||
assert w_p5_a1.liquid_history == [("l1", 40.0)]
|
||||
|
||||
# 返回 volumes 严格按输入 index 顺序回拼
|
||||
assert ret.volumes == [10.0, 20.0, 30.0, 40.0]
|
||||
|
||||
# plate dump:按 first-occurrence(plate_slot2 第 1 次出现于 idx=0,plate_slot3 idx=1,plate_slot5 idx=3)
|
||||
plate_names = [p["name"] for p in ret.plate]
|
||||
assert plate_names == ["plate_slot2", "plate_slot3", "plate_slot5"]
|
||||
|
||||
|
||||
def test_grouped_by_plate_volumes_clamped_to_max_volume(handler):
|
||||
"""``set_liquid`` 会按 ``max_volume`` 做 clamp,防止初始化液量超容器容量。"""
|
||||
plate = DummyPlate(name="plate_slot2")
|
||||
well = DummyWell(name="A1", parent=plate, max_volume=200.0)
|
||||
|
||||
ret = handler._set_liquid_grouped_by_plate(
|
||||
wells=[well],
|
||||
liquid_names=["overflow"],
|
||||
volumes=[500.0], # 超过 max_volume=200
|
||||
)
|
||||
|
||||
assert well.liquid_history == [("overflow", 200.0)]
|
||||
assert ret.volumes == [200.0]
|
||||
|
||||
|
||||
def test_grouped_by_plate_empty_names_short_circuit(handler):
|
||||
"""``liquid_names`` 与 ``volumes`` 均为空:早返回,wells 列表回显但不设液。"""
|
||||
wells = _wells_grid("plate_slot2", ["A1", "A2"])
|
||||
ret = handler._set_liquid_grouped_by_plate(
|
||||
wells=wells,
|
||||
liquid_names=[],
|
||||
volumes=[],
|
||||
)
|
||||
# 不调用 set_liquids
|
||||
assert all(w.liquid_history == [] for w in wells)
|
||||
assert ret.volumes == []
|
||||
# wells dump 仍返回输入列表
|
||||
assert [w["name"] for w in ret.wells] == ["A1", "A2"]
|
||||
|
||||
|
||||
def test_grouped_by_plate_length_mismatch_raises(handler):
|
||||
"""wells / liquid_names / volumes 长度不一致应直接 raise(防御性校验)。"""
|
||||
wells = _wells_grid("plate_slot2", ["A1", "A2"])
|
||||
with pytest.raises(ValueError, match=r"必须等长"):
|
||||
handler._set_liquid_grouped_by_plate(
|
||||
wells=wells,
|
||||
liquid_names=["r"] * 2,
|
||||
volumes=[10.0], # 长度 1,不匹配
|
||||
)
|
||||
566
tests/devices/liquid_handling/test_tip_reuse_by_liquid_name.py
Normal file
566
tests/devices/liquid_handling/test_tip_reuse_by_liquid_name.py
Normal file
@@ -0,0 +1,566 @@
|
||||
"""P10 v2 — Tip 复用 ``tracker.liquids`` 等价规则单元测试。
|
||||
|
||||
测试覆盖(详见 ``product_designs/protocol_convert/10-tip-reuse-by-liquid-history.md`` §5):
|
||||
|
||||
- Helper:``is_known_liquid_name`` / ``same_liquid_via_liquids`` /
|
||||
``same_liquid_via_liquids_pair`` / ``capture_tip_liquid_name``(4 helper
|
||||
位于 ``liquid_history.py``,PLR-free 模块)。
|
||||
- 单通道 transfer_liquid 主循环:identity-keep / liquids-keep / 配置开关 /
|
||||
未知 name 保守换 tip / aspirate 顶层归零时序。
|
||||
- 8 通道分支:段锚孔 liquids-keep。
|
||||
- 跨节点边界:两个独立 transfer_liquid 调用状态隔离。
|
||||
|
||||
helper 测试独立于 PLR,可在 ``pylabrobot`` 缺失环境下单独运行;端到端
|
||||
``transfer_liquid`` 主循环测试需要 PLR 环境(沿用 ``test_transfer_liquid.py`` 的
|
||||
``FakeLiquidHandler`` 模式:跳过 ``super().__init__``,仅 stub 4 类方法记录调用)。
|
||||
若 PLR import 失败则自动 skip 端到端测试,保留 helper 测试结果。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Iterable, List, Optional, Sequence, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
# P10 v2 helper 位于 PLR-free 模块,无论 pylabrobot 是否安装都能 import。
|
||||
from unilabos.devices.liquid_handling.liquid_history import (
|
||||
capture_tip_liquid_name,
|
||||
is_known_liquid_name,
|
||||
same_liquid_via_liquids,
|
||||
same_liquid_via_liquids_pair,
|
||||
)
|
||||
|
||||
# 端到端测试依赖 PLR 完整环境;若 import 失败(例如本地 PLR 版本不匹配),
|
||||
# 整段端到端测试自动 skip,但 helper 测试照常执行。
|
||||
try:
|
||||
from unilabos.devices.liquid_handling.liquid_handler_abstract import (
|
||||
LiquidHandlerAbstract,
|
||||
)
|
||||
|
||||
_PLR_AVAILABLE = True
|
||||
_PLR_IMPORT_ERROR: Optional[Exception] = None
|
||||
except Exception as exc: # pragma: no cover - 环境相关
|
||||
LiquidHandlerAbstract = None # type: ignore[assignment, misc]
|
||||
_PLR_AVAILABLE = False
|
||||
_PLR_IMPORT_ERROR = exc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures:DummyTracker / DummyWell / DummyTipSpot / FakeLiquidHandler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class DummyTracker:
|
||||
"""模拟 PLR ``VolumeTracker``:仅暴露 P10 v2 关心的 ``liquids`` 字段。"""
|
||||
|
||||
liquids: List[Tuple[Any, float]] = field(default_factory=list)
|
||||
max_volume: float = 200.0
|
||||
is_disabled: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class DummyWell:
|
||||
"""模拟 PLR ``Well``:仅暴露 ``tracker``。"""
|
||||
|
||||
name: str = "well"
|
||||
tracker: DummyTracker = field(default_factory=DummyTracker)
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover
|
||||
return f"DummyWell({self.name})"
|
||||
|
||||
|
||||
def make_well(name: str, liquid_name: Optional[str] = None, vol: float = 100.0) -> DummyWell:
|
||||
"""构造一个 well;若指定 ``liquid_name`` 则写入 ``tracker.liquids`` 顶层。"""
|
||||
well = DummyWell(name=name, tracker=DummyTracker())
|
||||
if liquid_name is not None:
|
||||
well.tracker.liquids = [(liquid_name, vol)]
|
||||
return well
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DummyTipSpot:
|
||||
name: str
|
||||
|
||||
|
||||
def make_tip_iter(n: int = 256) -> Iterable[List[DummyTipSpot]]:
|
||||
for i in range(n):
|
||||
yield [DummyTipSpot(f"tip_{i}")]
|
||||
|
||||
|
||||
# E2E 测试用的 base:PLR 可用时是 ``LiquidHandlerAbstract``,否则 fallback 到
|
||||
# ``object`` 让模块仍能 import;带 ``LiquidHandlerAbstract`` 的 e2e 测试用
|
||||
# ``skipif`` 跳过。
|
||||
_FakeBase = LiquidHandlerAbstract if _PLR_AVAILABLE else object
|
||||
|
||||
|
||||
class FakeLiquidHandler(_FakeBase): # type: ignore[misc, valid-type]
|
||||
"""不初始化真实 backend/deck;仅记录 transfer_liquid 内部 4 类调用序列。
|
||||
|
||||
P10 v2 测试关心 ``pick_up_tips`` / ``discard_tips`` 的触发次数 + 顺序,
|
||||
以推断 tip 是否被复用(一次 pick_up_tips 多次 aspirate/dispense → 复用)。
|
||||
"""
|
||||
|
||||
def __init__(self, channel_num: int = 1, tip_reuse_by_liquid_name: bool = True):
|
||||
# 不调用 super().__init__,避免硬件 / ROS / PLR Deck 初始化。
|
||||
self.channel_num = channel_num
|
||||
self.support_touch_tip = True
|
||||
self.current_tip = iter(make_tip_iter(2048))
|
||||
self.calls: List[Tuple[str, Any]] = []
|
||||
self._tip_reuse_by_liquid_name: bool = tip_reuse_by_liquid_name
|
||||
|
||||
def set_tiprack(self, tip_racks):
|
||||
if not tip_racks:
|
||||
return
|
||||
# 跳过真实 set_tiprack(依赖 PLR Deck)
|
||||
return
|
||||
|
||||
async def pick_up_tips(self, tip_spots, use_channels=None, offsets=None, **kw):
|
||||
self.calls.append(("pick_up_tips", {"tips": list(tip_spots), "use_channels": use_channels}))
|
||||
|
||||
async def aspirate(
|
||||
self,
|
||||
resources: Sequence[Any],
|
||||
vols: List[float],
|
||||
use_channels: Optional[List[int]] = None,
|
||||
flow_rates: Optional[List[Optional[float]]] = None,
|
||||
offsets: Any = None,
|
||||
liquid_height: Any = None,
|
||||
blow_out_air_volume: Any = None,
|
||||
spread: str = "wide",
|
||||
**backend_kwargs,
|
||||
):
|
||||
self.calls.append(
|
||||
("aspirate", {"resources": list(resources), "vols": list(vols)})
|
||||
)
|
||||
|
||||
async def dispense(
|
||||
self,
|
||||
resources: Sequence[Any],
|
||||
vols: List[float],
|
||||
use_channels: Optional[List[int]] = None,
|
||||
flow_rates: Optional[List[Optional[float]]] = None,
|
||||
offsets: Any = None,
|
||||
liquid_height: Any = None,
|
||||
blow_out_air_volume: Any = None,
|
||||
spread: str = "wide",
|
||||
**backend_kwargs,
|
||||
):
|
||||
self.calls.append(
|
||||
("dispense", {"resources": list(resources), "vols": list(vols)})
|
||||
)
|
||||
|
||||
async def discard_tips(self, use_channels=None, *args, **kwargs):
|
||||
self.calls.append(("discard_tips", {"use_channels": use_channels}))
|
||||
|
||||
|
||||
class AspiratePopFakeLiquidHandler(FakeLiquidHandler):
|
||||
"""T11 专用:aspirate 时模拟 PLR "顶层归零时 pop ``tracker.liquids`` 顶层" 的行为。
|
||||
|
||||
用于验证 P10 v2 的关键时序约束:tip name 必须在 aspirate **之前**预读,
|
||||
否则 aspirate 后再读 ``tracker.liquids[-1]`` 会拿不到液体身份。
|
||||
"""
|
||||
|
||||
async def aspirate(self, resources, vols, **kwargs):
|
||||
await super().aspirate(resources, vols, **kwargs)
|
||||
# 模拟 PLR 顶层归零时 pop:对每个 source well,若 liquids 非空则 pop 顶层
|
||||
for r in resources:
|
||||
tracker = getattr(r, "tracker", None)
|
||||
if tracker is not None and tracker.liquids:
|
||||
tracker.liquids.pop()
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
def call_names(lh: FakeLiquidHandler) -> List[str]:
|
||||
return [c[0] for c in lh.calls]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper 单元测试
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestIsKnownLiquidName:
|
||||
def test_empty_string_is_unknown(self) -> None:
|
||||
assert is_known_liquid_name("") is False
|
||||
|
||||
def test_none_is_unknown(self) -> None:
|
||||
assert is_known_liquid_name(None) is False
|
||||
|
||||
def test_literal_unknown_is_unknown(self) -> None:
|
||||
assert is_known_liquid_name("unknown") is False
|
||||
assert is_known_liquid_name("UNKNOWN") is False
|
||||
assert is_known_liquid_name(" Unknown ") is False
|
||||
|
||||
def test_literal_none_string_is_unknown(self) -> None:
|
||||
assert is_known_liquid_name("none") is False
|
||||
assert is_known_liquid_name("None") is False
|
||||
|
||||
def test_real_liquid_name_is_known(self) -> None:
|
||||
assert is_known_liquid_name("PBS") is True
|
||||
assert is_known_liquid_name("Tris HCl") is True
|
||||
assert is_known_liquid_name("Liquid_3") is True
|
||||
|
||||
|
||||
class TestSameLiquidViaLiquids:
|
||||
def test_well_and_tip_same_name_match(self) -> None:
|
||||
well = make_well("A1", "PBS")
|
||||
assert same_liquid_via_liquids(well, "PBS") is True
|
||||
|
||||
def test_well_and_tip_different_names_no_match(self) -> None:
|
||||
well = make_well("A1", "PBS")
|
||||
assert same_liquid_via_liquids(well, "Tris HCl") is False
|
||||
|
||||
def test_tip_unknown_returns_false(self) -> None:
|
||||
well = make_well("A1", "PBS")
|
||||
assert same_liquid_via_liquids(well, None) is False
|
||||
assert same_liquid_via_liquids(well, "") is False
|
||||
assert same_liquid_via_liquids(well, "unknown") is False
|
||||
|
||||
def test_well_empty_liquids_returns_false(self) -> None:
|
||||
well = make_well("A1", liquid_name=None) # 不写 liquids
|
||||
assert same_liquid_via_liquids(well, "PBS") is False
|
||||
|
||||
def test_well_unknown_literal_returns_false(self) -> None:
|
||||
well = make_well("A1", "unknown")
|
||||
assert same_liquid_via_liquids(well, "unknown") is False
|
||||
|
||||
|
||||
class TestSameLiquidViaLiquidsPair:
|
||||
def test_two_wells_same_name_match(self) -> None:
|
||||
a = make_well("A1", "PBS")
|
||||
b = make_well("B1", "PBS")
|
||||
assert same_liquid_via_liquids_pair(a, b) is True
|
||||
|
||||
def test_two_wells_different_names_no_match(self) -> None:
|
||||
a = make_well("A1", "PBS")
|
||||
b = make_well("B1", "Tris HCl")
|
||||
assert same_liquid_via_liquids_pair(a, b) is False
|
||||
|
||||
def test_either_well_empty_returns_false(self) -> None:
|
||||
a = make_well("A1", "PBS")
|
||||
b = make_well("B1", liquid_name=None)
|
||||
assert same_liquid_via_liquids_pair(a, b) is False
|
||||
assert same_liquid_via_liquids_pair(b, a) is False
|
||||
|
||||
|
||||
class TestCaptureTipLiquidName:
|
||||
def test_known_name_returned(self) -> None:
|
||||
well = make_well("A1", "PBS")
|
||||
assert capture_tip_liquid_name(well) == "PBS"
|
||||
|
||||
def test_empty_well_returns_none(self) -> None:
|
||||
well = make_well("A1", liquid_name=None)
|
||||
assert capture_tip_liquid_name(well) is None
|
||||
|
||||
def test_unknown_literal_returns_none(self) -> None:
|
||||
well = make_well("A1", "unknown")
|
||||
assert capture_tip_liquid_name(well) is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T1–T12 端到端测试(单通道 transfer_liquid 主循环)
|
||||
#
|
||||
# 需要 PLR 完整环境(``pylabrobot.liquid_handling.LiquidHandlerBackend`` 等)。
|
||||
# 若 PLR import 失败则整段 skip,helper 测试照常运行。
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_skip_if_no_plr = pytest.mark.skipif(
|
||||
not _PLR_AVAILABLE,
|
||||
reason=f"pylabrobot import failed: {_PLR_IMPORT_ERROR}",
|
||||
)
|
||||
|
||||
|
||||
@_skip_if_no_plr
|
||||
class TestSingleChannelTipReuse:
|
||||
"""覆盖 §5 矩阵 T1 / T2 / T3 / T4 / T5 / T6 / T8 / T10 / T11。"""
|
||||
|
||||
def test_T1_identity_hit_reuses_tip(self) -> None:
|
||||
"""T1:连续 2 轮同 source/target → identity-keep 命中,复用 tip。"""
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
src = make_well("S0", "PBS")
|
||||
tgt = make_well("T0")
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=[src, src],
|
||||
targets=[tgt, tgt],
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=[1, 1],
|
||||
dis_vols=[1, 1],
|
||||
)
|
||||
)
|
||||
# 2 次 transfer,但 identity-keep → 仅 1 次 pick_up_tips / 1 次 discard_tips
|
||||
assert call_names(lh).count("pick_up_tips") == 1
|
||||
assert call_names(lh).count("discard_tips") == 1
|
||||
assert call_names(lh).count("aspirate") == 2
|
||||
assert call_names(lh).count("dispense") == 2
|
||||
|
||||
def test_T2_liquids_hit_across_plates(self) -> None:
|
||||
"""T2:9 个独立 source well(不同 PLR Well 对象)都装 PBS → identity 全 fail,liquids-keep 全命中。"""
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
sources = [make_well(f"S{i}", "PBS") for i in range(9)]
|
||||
targets = [make_well(f"T{i}") for i in range(9)]
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=[1] * 9,
|
||||
dis_vols=[1] * 9,
|
||||
)
|
||||
)
|
||||
# 9 个 source 物理上同液 → 整段共用 1 个 tip
|
||||
assert call_names(lh).count("pick_up_tips") == 1
|
||||
assert call_names(lh).count("discard_tips") == 1
|
||||
assert call_names(lh).count("aspirate") == 9
|
||||
assert call_names(lh).count("dispense") == 9
|
||||
|
||||
def test_T3_liquids_hit_same_plate_different_wells(self) -> None:
|
||||
"""T3:同 plate 上 A1-H1 都装 PBS(8 个不同 Well 对象)→ identity 全 fail,liquids-keep 命中。"""
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
sources = [make_well(f"A{i}", "PBS") for i in range(1, 9)]
|
||||
targets = [make_well(f"T{i}") for i in range(8)]
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=[1] * 8,
|
||||
dis_vols=[1] * 8,
|
||||
)
|
||||
)
|
||||
assert call_names(lh).count("pick_up_tips") == 1
|
||||
assert call_names(lh).count("discard_tips") == 1
|
||||
|
||||
def test_T4_liquids_not_match_forces_tip_change(self) -> None:
|
||||
"""T4:A1=PBS,B1=Tris HCl → liquids 名不等,强制换 tip。"""
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
sources = [make_well("A1", "PBS"), make_well("B1", "Tris HCl")]
|
||||
targets = [make_well("T0"), make_well("T1")]
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=[1, 1],
|
||||
dis_vols=[1, 1],
|
||||
)
|
||||
)
|
||||
# 2 次完全独立的 transfer:2 次 pick_up / 2 次 discard
|
||||
assert call_names(lh).count("pick_up_tips") == 2
|
||||
assert call_names(lh).count("discard_tips") == 2
|
||||
|
||||
def test_T5_empty_liquids_forces_tip_change(self) -> None:
|
||||
"""T5:source 从未调过 set_liquids(liquids 空)→ 视为未知,强制换 tip。"""
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
sources = [make_well("A1"), make_well("B1")] # 没装液体名
|
||||
targets = [make_well("T0"), make_well("T1")]
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=[1, 1],
|
||||
dis_vols=[1, 1],
|
||||
)
|
||||
)
|
||||
assert call_names(lh).count("pick_up_tips") == 2
|
||||
assert call_names(lh).count("discard_tips") == 2
|
||||
|
||||
def test_T6_switch_off_disables_liquids_keep(self) -> None:
|
||||
"""T6:tip_reuse_by_liquid_name=False,T2 场景退化为 identity-only,强制换 tip。"""
|
||||
lh = FakeLiquidHandler(channel_num=1, tip_reuse_by_liquid_name=False)
|
||||
sources = [make_well(f"S{i}", "PBS") for i in range(9)]
|
||||
targets = [make_well(f"T{i}") for i in range(9)]
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=[1] * 9,
|
||||
dis_vols=[1] * 9,
|
||||
)
|
||||
)
|
||||
# 关闭开关后 → 退化为 identity-only,9 次独立换 tip
|
||||
assert call_names(lh).count("pick_up_tips") == 9
|
||||
assert call_names(lh).count("discard_tips") == 9
|
||||
|
||||
def test_T8_mix_style_same_source_reuses_via_identity(self) -> None:
|
||||
"""T8:单 source 反复 aspirate/dispense → identity-keep 命中(mix-style)。"""
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
src = make_well("S0", "Methanol")
|
||||
tgt = make_well("T0")
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=[src, src, src],
|
||||
targets=[tgt, tgt, tgt],
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=[1, 1, 1],
|
||||
dis_vols=[1, 1, 1],
|
||||
)
|
||||
)
|
||||
assert call_names(lh).count("pick_up_tips") == 1
|
||||
assert call_names(lh).count("discard_tips") == 1
|
||||
|
||||
def test_T10_unknown_literal_treated_as_unknown(self) -> None:
|
||||
"""T10:``tracker.liquids = [("unknown", v)]``(兼容旧数据)→ 视为未知,强制换 tip。"""
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
sources = [make_well("A1", "unknown"), make_well("B1", "unknown")]
|
||||
targets = [make_well("T0"), make_well("T1")]
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=[1, 1],
|
||||
dis_vols=[1, 1],
|
||||
)
|
||||
)
|
||||
assert call_names(lh).count("pick_up_tips") == 2
|
||||
assert call_names(lh).count("discard_tips") == 2
|
||||
|
||||
def test_T11_aspirate_pop_timing_pre_read(self) -> None:
|
||||
"""T11:aspirate 顶层归零 → PLR pop ``tracker.liquids`` 顶层;
|
||||
验证 P10 v2 ``pending_tip_name`` 必须在 aspirate **之前**预读才能命中下一轮。
|
||||
"""
|
||||
lh = AspiratePopFakeLiquidHandler(channel_num=1)
|
||||
sources = [make_well(f"S{i}", "PBS") for i in range(3)]
|
||||
targets = [make_well(f"T{i}") for i in range(3)]
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=[1] * 3,
|
||||
dis_vols=[1] * 3,
|
||||
)
|
||||
)
|
||||
# 即使 aspirate 后 source.tracker.liquids 被 pop,pending_tip_name 已捕获 "PBS"
|
||||
# → 下一轮 source 仍是 PBS(aspirate 还没发生),liquids-keep 命中
|
||||
# → 整段 1 次 pick_up_tips
|
||||
assert call_names(lh).count("pick_up_tips") == 1
|
||||
assert call_names(lh).count("discard_tips") == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T7:跨节点边界(两个独立 transfer_liquid 调用,状态隔离)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@_skip_if_no_plr
|
||||
class TestCrossNodeBoundary:
|
||||
"""T7:两个 transfer_liquid 节点之间不复用 tip(每次调用初始化 current_tip_liquid_name=None)。"""
|
||||
|
||||
def test_T7_two_calls_dont_share_tip_state(self) -> None:
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
src_a = make_well("A_src", "PBS")
|
||||
tgt_a = make_well("A_tgt")
|
||||
src_b = make_well("B_src", "PBS") # 同名液,但不同 well
|
||||
tgt_b = make_well("B_tgt")
|
||||
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=[src_a],
|
||||
targets=[tgt_a],
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=[1],
|
||||
dis_vols=[1],
|
||||
)
|
||||
)
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=[src_b],
|
||||
targets=[tgt_b],
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=[1],
|
||||
dis_vols=[1],
|
||||
)
|
||||
)
|
||||
# 两次调用各自独立换 tip → 2 次 pick_up_tips / 2 次 discard_tips
|
||||
assert call_names(lh).count("pick_up_tips") == 2
|
||||
assert call_names(lh).count("discard_tips") == 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T9:8 通道段锚孔 liquids-keep
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@_skip_if_no_plr
|
||||
class TestEightChannelSegmentTipReuse:
|
||||
"""T9:8 通道分段,连续两段 src_slice[0] 同名 → 段间不换 tip。"""
|
||||
|
||||
def test_T9_two_segments_same_anchor_liquid(self) -> None:
|
||||
lh = FakeLiquidHandler(channel_num=8)
|
||||
# 16 个 source wells,分 2 段;段 1 锚孔 = sources[0],段 2 锚孔 = sources[8]
|
||||
sources = [make_well(f"S{i}", "PBS") for i in range(16)]
|
||||
targets = [make_well(f"T{i}") for i in range(16)]
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=list(range(8)),
|
||||
asp_vols=[1] * 16,
|
||||
dis_vols=[1] * 16,
|
||||
mix_times=0,
|
||||
)
|
||||
)
|
||||
# 2 段都同液 → liquids-keep 命中 → 仅 1 次 pick_up_tips
|
||||
assert call_names(lh).count("pick_up_tips") == 1
|
||||
assert call_names(lh).count("discard_tips") == 1
|
||||
|
||||
def test_T9b_two_segments_different_anchor_liquid_forces_tip_change(self) -> None:
|
||||
"""T9b:段 1 锚孔 = PBS,段 2 锚孔 = Tris → 段间强制换 tip。"""
|
||||
lh = FakeLiquidHandler(channel_num=8)
|
||||
seg1 = [make_well(f"S{i}", "PBS") for i in range(8)]
|
||||
seg2 = [make_well(f"S{i + 8}", "Tris HCl") for i in range(8)]
|
||||
sources = seg1 + seg2
|
||||
targets = [make_well(f"T{i}") for i in range(16)]
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=list(range(8)),
|
||||
asp_vols=[1] * 16,
|
||||
dis_vols=[1] * 16,
|
||||
mix_times=0,
|
||||
)
|
||||
)
|
||||
# 2 段不同液 → 2 次独立换 tip
|
||||
assert call_names(lh).count("pick_up_tips") == 2
|
||||
assert call_names(lh).count("discard_tips") == 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 配置开关默认值 / 实例字段读取
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@_skip_if_no_plr
|
||||
class TestConfigDefault:
|
||||
def test_default_switch_is_on(self) -> None:
|
||||
"""默认 ``_tip_reuse_by_liquid_name`` 应为 True(测试 fixture 显式 default 一致)。"""
|
||||
lh = FakeLiquidHandler()
|
||||
assert lh._tip_reuse_by_liquid_name is True
|
||||
|
||||
def test_switch_off_takes_effect(self) -> None:
|
||||
lh = FakeLiquidHandler(tip_reuse_by_liquid_name=False)
|
||||
assert lh._tip_reuse_by_liquid_name is False
|
||||
@@ -39,6 +39,11 @@ class FakeLiquidHandler(LiquidHandlerAbstract):
|
||||
self.current_tip = iter(make_tip_iter())
|
||||
self.calls: List[Tuple[str, Any]] = []
|
||||
|
||||
def set_tiprack(self, tip_racks):
|
||||
if not tip_racks:
|
||||
return
|
||||
super().set_tiprack(tip_racks)
|
||||
|
||||
async def pick_up_tips(self, tip_spots, use_channels=None, offsets=None, **backend_kwargs):
|
||||
self.calls.append(("pick_up_tips", {"tips": list(tip_spots), "use_channels": use_channels}))
|
||||
|
||||
|
||||
608
tests/devices/liquid_handling/unit_test.py
Normal file
608
tests/devices/liquid_handling/unit_test.py
Normal file
@@ -0,0 +1,608 @@
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Iterable, List, Optional, Sequence, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DummyContainer:
|
||||
name: str
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover
|
||||
return f"DummyContainer({self.name})"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DummyTipSpot:
|
||||
name: str
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover
|
||||
return f"DummyTipSpot({self.name})"
|
||||
|
||||
|
||||
def make_tip_iter(n: int = 256) -> Iterable[List[DummyTipSpot]]:
|
||||
"""Yield lists so code can safely call `tip.extend(next(self.current_tip))`."""
|
||||
for i in range(n):
|
||||
yield [DummyTipSpot(f"tip_{i}")]
|
||||
|
||||
|
||||
class FakeLiquidHandler(LiquidHandlerAbstract):
|
||||
"""不初始化真实 backend/deck;仅用来记录 transfer_liquid 内部调用序列。"""
|
||||
|
||||
def __init__(self, channel_num: int = 8):
|
||||
# 不调用 super().__init__,避免真实硬件/后端依赖
|
||||
self.channel_num = channel_num
|
||||
self.support_touch_tip = True
|
||||
self.current_tip = iter(make_tip_iter())
|
||||
self.calls: List[Tuple[str, Any]] = []
|
||||
|
||||
def set_tiprack(self, tip_racks):
|
||||
# transfer_liquid 总会调用 set_tiprack;测试用 Dummy 枪头时 tip_racks 为空,需保留自种子的 current_tip
|
||||
if not tip_racks:
|
||||
return
|
||||
super().set_tiprack(tip_racks)
|
||||
|
||||
async def pick_up_tips(self, tip_spots, use_channels=None, offsets=None, **backend_kwargs):
|
||||
self.calls.append(("pick_up_tips", {"tips": list(tip_spots), "use_channels": use_channels}))
|
||||
|
||||
async def aspirate(
|
||||
self,
|
||||
resources: Sequence[Any],
|
||||
vols: List[float],
|
||||
use_channels: Optional[List[int]] = None,
|
||||
flow_rates: Optional[List[Optional[float]]] = None,
|
||||
offsets: Any = None,
|
||||
liquid_height: Any = None,
|
||||
blow_out_air_volume: Any = None,
|
||||
spread: str = "wide",
|
||||
**backend_kwargs,
|
||||
):
|
||||
self.calls.append(
|
||||
(
|
||||
"aspirate",
|
||||
{
|
||||
"resources": list(resources),
|
||||
"vols": list(vols),
|
||||
"use_channels": list(use_channels) if use_channels is not None else None,
|
||||
"flow_rates": list(flow_rates) if flow_rates is not None else None,
|
||||
"offsets": list(offsets) if offsets is not None else None,
|
||||
"liquid_height": list(liquid_height) if liquid_height is not None else None,
|
||||
"blow_out_air_volume": list(blow_out_air_volume) if blow_out_air_volume is not None else None,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
async def dispense(
|
||||
self,
|
||||
resources: Sequence[Any],
|
||||
vols: List[float],
|
||||
use_channels: Optional[List[int]] = None,
|
||||
flow_rates: Optional[List[Optional[float]]] = None,
|
||||
offsets: Any = None,
|
||||
liquid_height: Any = None,
|
||||
blow_out_air_volume: Any = None,
|
||||
spread: str = "wide",
|
||||
**backend_kwargs,
|
||||
):
|
||||
self.calls.append(
|
||||
(
|
||||
"dispense",
|
||||
{
|
||||
"resources": list(resources),
|
||||
"vols": list(vols),
|
||||
"use_channels": list(use_channels) if use_channels is not None else None,
|
||||
"flow_rates": list(flow_rates) if flow_rates is not None else None,
|
||||
"offsets": list(offsets) if offsets is not None else None,
|
||||
"liquid_height": list(liquid_height) if liquid_height is not None else None,
|
||||
"blow_out_air_volume": list(blow_out_air_volume) if blow_out_air_volume is not None else None,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
async def discard_tips(self, use_channels=None, *args, **kwargs):
|
||||
# 有的分支是 discard_tips(use_channels=[0]),有的分支是 discard_tips([0..7])(位置参数)
|
||||
self.calls.append(("discard_tips", {"use_channels": list(use_channels) if use_channels is not None else None}))
|
||||
|
||||
async def custom_delay(self, seconds=0, msg=None):
|
||||
self.calls.append(("custom_delay", {"seconds": seconds, "msg": msg}))
|
||||
|
||||
async def touch_tip(self, targets):
|
||||
# 原实现会访问 targets.get_size_x() 等;测试里只记录调用
|
||||
self.calls.append(("touch_tip", {"targets": targets}))
|
||||
|
||||
def run(coro):
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
def test_one_to_one_single_channel_basic_calls():
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
lh.current_tip = iter(make_tip_iter(64))
|
||||
|
||||
sources = [DummyContainer(f"S{i}") for i in range(3)]
|
||||
targets = [DummyContainer(f"T{i}") for i in range(3)]
|
||||
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=[1, 2, 3],
|
||||
dis_vols=[4, 5, 6],
|
||||
mix_times=None, # 应该仍能执行(不 mix)
|
||||
)
|
||||
)
|
||||
|
||||
assert [c[0] for c in lh.calls].count("pick_up_tips") == 3
|
||||
assert [c[0] for c in lh.calls].count("aspirate") == 3
|
||||
assert [c[0] for c in lh.calls].count("dispense") == 3
|
||||
assert [c[0] for c in lh.calls].count("discard_tips") == 3
|
||||
|
||||
# 每次 aspirate/dispense 都是单孔列表
|
||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
||||
assert aspirates[0]["resources"] == [sources[0]]
|
||||
assert aspirates[0]["vols"] == [1.0]
|
||||
|
||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
||||
assert dispenses[2]["resources"] == [targets[2]]
|
||||
assert dispenses[2]["vols"] == [6.0]
|
||||
|
||||
|
||||
def test_one_to_one_single_channel_before_stage_mixes_prior_to_aspirate():
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
lh.current_tip = iter(make_tip_iter(16))
|
||||
|
||||
source = DummyContainer("S0")
|
||||
target = DummyContainer("T0")
|
||||
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=[source],
|
||||
targets=[target],
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=[5],
|
||||
dis_vols=[5],
|
||||
mix_stage="before",
|
||||
mix_times=1,
|
||||
mix_vol=3,
|
||||
)
|
||||
)
|
||||
|
||||
aspirate_calls = [(idx, payload) for idx, (name, payload) in enumerate(lh.calls) if name == "aspirate"]
|
||||
assert len(aspirate_calls) >= 2
|
||||
mix_idx, mix_payload = aspirate_calls[0]
|
||||
assert mix_payload["resources"] == [target]
|
||||
assert mix_payload["vols"] == [3]
|
||||
transfer_idx, transfer_payload = aspirate_calls[1]
|
||||
assert transfer_payload["resources"] == [source]
|
||||
assert mix_idx < transfer_idx
|
||||
|
||||
|
||||
def test_one_to_one_eight_channel_groups_by_8():
|
||||
lh = FakeLiquidHandler(channel_num=8)
|
||||
lh.current_tip = iter(make_tip_iter(256))
|
||||
|
||||
sources = [DummyContainer(f"S{i}") for i in range(16)]
|
||||
targets = [DummyContainer(f"T{i}") for i in range(16)]
|
||||
asp_vols = list(range(1, 17))
|
||||
dis_vols = list(range(101, 117))
|
||||
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=list(range(8)),
|
||||
asp_vols=asp_vols,
|
||||
dis_vols=dis_vols,
|
||||
mix_times=0, # 触发逻辑但不 mix
|
||||
)
|
||||
)
|
||||
|
||||
# 16 个任务 -> 2 组,每组 8 通道一起做
|
||||
assert [c[0] for c in lh.calls].count("pick_up_tips") == 2
|
||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
||||
assert len(aspirates) == 2
|
||||
assert len(dispenses) == 2
|
||||
|
||||
assert aspirates[0]["resources"] == sources[0:8]
|
||||
assert aspirates[0]["vols"] == [float(v) for v in asp_vols[0:8]]
|
||||
assert dispenses[1]["resources"] == targets[8:16]
|
||||
assert dispenses[1]["vols"] == [float(v) for v in dis_vols[8:16]]
|
||||
|
||||
|
||||
def test_one_to_one_eight_channel_requires_multiple_of_8_targets():
|
||||
lh = FakeLiquidHandler(channel_num=8)
|
||||
lh.current_tip = iter(make_tip_iter(64))
|
||||
|
||||
sources = [DummyContainer(f"S{i}") for i in range(9)]
|
||||
targets = [DummyContainer(f"T{i}") for i in range(9)]
|
||||
|
||||
with pytest.raises(ValueError, match="multiple of 8"):
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=list(range(8)),
|
||||
asp_vols=[1] * 9,
|
||||
dis_vols=[1] * 9,
|
||||
mix_times=0,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_one_to_one_eight_channel_parameter_lists_are_chunked_per_8():
|
||||
lh = FakeLiquidHandler(channel_num=8)
|
||||
lh.current_tip = iter(make_tip_iter(512))
|
||||
|
||||
sources = [DummyContainer(f"S{i}") for i in range(16)]
|
||||
targets = [DummyContainer(f"T{i}") for i in range(16)]
|
||||
asp_vols = [i + 1 for i in range(16)]
|
||||
dis_vols = [200 + i for i in range(16)]
|
||||
asp_flow_rates = [0.1 * (i + 1) for i in range(16)]
|
||||
dis_flow_rates = [0.2 * (i + 1) for i in range(16)]
|
||||
offsets = [f"offset_{i}" for i in range(16)]
|
||||
liquid_heights = [i * 0.5 for i in range(16)]
|
||||
blow_out_air_volume = [i + 0.05 for i in range(16)]
|
||||
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=list(range(8)),
|
||||
asp_vols=asp_vols,
|
||||
dis_vols=dis_vols,
|
||||
asp_flow_rates=asp_flow_rates,
|
||||
dis_flow_rates=dis_flow_rates,
|
||||
offsets=offsets,
|
||||
liquid_height=liquid_heights,
|
||||
blow_out_air_volume=blow_out_air_volume,
|
||||
mix_times=0,
|
||||
)
|
||||
)
|
||||
|
||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
||||
assert len(aspirates) == len(dispenses) == 2
|
||||
|
||||
for batch_idx in range(2):
|
||||
start = batch_idx * 8
|
||||
end = start + 8
|
||||
asp_call = aspirates[batch_idx]
|
||||
dis_call = dispenses[batch_idx]
|
||||
assert asp_call["resources"] == sources[start:end]
|
||||
assert asp_call["flow_rates"] == asp_flow_rates[start:end]
|
||||
assert asp_call["offsets"] == offsets[start:end]
|
||||
assert asp_call["liquid_height"] == liquid_heights[start:end]
|
||||
assert asp_call["blow_out_air_volume"] == blow_out_air_volume[start:end]
|
||||
assert dis_call["flow_rates"] == dis_flow_rates[start:end]
|
||||
assert dis_call["offsets"] == offsets[start:end]
|
||||
assert dis_call["liquid_height"] == liquid_heights[start:end]
|
||||
assert dis_call["blow_out_air_volume"] == blow_out_air_volume[start:end]
|
||||
|
||||
|
||||
def test_one_to_one_eight_channel_handles_32_tasks_four_batches():
|
||||
lh = FakeLiquidHandler(channel_num=8)
|
||||
lh.current_tip = iter(make_tip_iter(1024))
|
||||
|
||||
sources = [DummyContainer(f"S{i}") for i in range(32)]
|
||||
targets = [DummyContainer(f"T{i}") for i in range(32)]
|
||||
asp_vols = [i + 1 for i in range(32)]
|
||||
dis_vols = [300 + i for i in range(32)]
|
||||
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=list(range(8)),
|
||||
asp_vols=asp_vols,
|
||||
dis_vols=dis_vols,
|
||||
mix_times=0,
|
||||
)
|
||||
)
|
||||
|
||||
pick_calls = [name for name, _ in lh.calls if name == "pick_up_tips"]
|
||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
||||
assert len(pick_calls) == 4
|
||||
assert len(aspirates) == len(dispenses) == 4
|
||||
assert aspirates[0]["resources"] == sources[0:8]
|
||||
assert aspirates[-1]["resources"] == sources[24:32]
|
||||
assert dispenses[0]["resources"] == targets[0:8]
|
||||
assert dispenses[-1]["resources"] == targets[24:32]
|
||||
|
||||
|
||||
def test_one_to_many_single_channel_aspirates_total_when_asp_vol_too_small():
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
lh.current_tip = iter(make_tip_iter(64))
|
||||
|
||||
source = DummyContainer("SRC")
|
||||
targets = [DummyContainer(f"T{i}") for i in range(3)]
|
||||
dis_vols = [10, 20, 30] # sum=60
|
||||
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=[source],
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=10, # 小于 sum(dis_vols) -> 应吸 60
|
||||
dis_vols=dis_vols,
|
||||
mix_times=0,
|
||||
)
|
||||
)
|
||||
|
||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
||||
assert len(aspirates) == 1
|
||||
assert aspirates[0]["resources"] == [source]
|
||||
assert aspirates[0]["vols"] == [60.0]
|
||||
assert aspirates[0]["use_channels"] == [0]
|
||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
||||
assert [d["vols"][0] for d in dispenses] == [10.0, 20.0, 30.0]
|
||||
|
||||
|
||||
def test_one_to_many_eight_channel_basic():
|
||||
lh = FakeLiquidHandler(channel_num=8)
|
||||
lh.current_tip = iter(make_tip_iter(128))
|
||||
|
||||
source = DummyContainer("SRC")
|
||||
targets = [DummyContainer(f"T{i}") for i in range(8)]
|
||||
dis_vols = [i + 1 for i in range(8)]
|
||||
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=[source],
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=list(range(8)),
|
||||
asp_vols=999, # one-to-many 8ch 会按 dis_vols 吸(每通道各自)
|
||||
dis_vols=dis_vols,
|
||||
mix_times=0,
|
||||
)
|
||||
)
|
||||
|
||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
||||
assert aspirates[0]["resources"] == [source] * 8
|
||||
assert aspirates[0]["vols"] == [float(v) for v in dis_vols]
|
||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
||||
assert dispenses[0]["resources"] == targets
|
||||
assert dispenses[0]["vols"] == [float(v) for v in dis_vols]
|
||||
|
||||
|
||||
def test_many_to_one_single_channel_standard_dispense_equals_asp_by_default():
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
lh.current_tip = iter(make_tip_iter(128))
|
||||
|
||||
sources = [DummyContainer(f"S{i}") for i in range(3)]
|
||||
target = DummyContainer("T")
|
||||
asp_vols = [5, 6, 7]
|
||||
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=[target],
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=asp_vols,
|
||||
dis_vols=1, # many-to-one 允许标量;非比例模式下实际每次分液=对应 asp_vol
|
||||
mix_times=0,
|
||||
)
|
||||
)
|
||||
|
||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
||||
assert [d["vols"][0] for d in dispenses] == [float(v) for v in asp_vols]
|
||||
assert all(d["resources"] == [target] for d in dispenses)
|
||||
|
||||
|
||||
def test_many_to_one_single_channel_before_stage_mixes_target_once():
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
lh.current_tip = iter(make_tip_iter(128))
|
||||
|
||||
sources = [DummyContainer("S0"), DummyContainer("S1")]
|
||||
target = DummyContainer("T")
|
||||
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=[target],
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=[5, 6],
|
||||
dis_vols=1,
|
||||
mix_stage="before",
|
||||
mix_times=2,
|
||||
mix_vol=4,
|
||||
)
|
||||
)
|
||||
|
||||
aspirate_calls = [(idx, payload) for idx, (name, payload) in enumerate(lh.calls) if name == "aspirate"]
|
||||
assert len(aspirate_calls) >= 1
|
||||
mix_idx, mix_payload = aspirate_calls[0]
|
||||
assert mix_payload["resources"] == [target]
|
||||
assert mix_payload["vols"] == [4]
|
||||
# 第一個 mix 之後會真正開始吸 source
|
||||
assert any(call["resources"] == [sources[0]] for _, call in aspirate_calls[1:])
|
||||
|
||||
|
||||
def test_many_to_one_single_channel_proportional_mixing_uses_dis_vols_per_source():
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
lh.current_tip = iter(make_tip_iter(128))
|
||||
|
||||
sources = [DummyContainer(f"S{i}") for i in range(3)]
|
||||
target = DummyContainer("T")
|
||||
asp_vols = [5, 6, 7]
|
||||
dis_vols = [1, 2, 3]
|
||||
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=[target],
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=asp_vols,
|
||||
dis_vols=dis_vols, # 比例模式
|
||||
mix_times=0,
|
||||
)
|
||||
)
|
||||
|
||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
||||
assert [d["vols"][0] for d in dispenses] == [float(v) for v in dis_vols]
|
||||
|
||||
|
||||
def test_many_to_one_eight_channel_basic():
|
||||
lh = FakeLiquidHandler(channel_num=8)
|
||||
lh.current_tip = iter(make_tip_iter(256))
|
||||
|
||||
sources = [DummyContainer(f"S{i}") for i in range(8)]
|
||||
target = DummyContainer("T")
|
||||
asp_vols = [10 + i for i in range(8)]
|
||||
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=[target],
|
||||
tip_racks=[],
|
||||
use_channels=list(range(8)),
|
||||
asp_vols=asp_vols,
|
||||
dis_vols=999, # 非比例模式下每通道分液=对应 asp_vol
|
||||
mix_times=0,
|
||||
)
|
||||
)
|
||||
|
||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
||||
assert aspirates[0]["resources"] == sources
|
||||
assert aspirates[0]["vols"] == [float(v) for v in asp_vols]
|
||||
assert dispenses[0]["resources"] == [target] * 8
|
||||
assert dispenses[0]["vols"] == [float(v) for v in asp_vols]
|
||||
|
||||
|
||||
def test_transfer_liquid_mode_detection_unsupported_shape_raises():
|
||||
lh = FakeLiquidHandler(channel_num=8)
|
||||
lh.current_tip = iter(make_tip_iter(64))
|
||||
|
||||
sources = [DummyContainer("S0"), DummyContainer("S1")]
|
||||
targets = [DummyContainer("T0"), DummyContainer("T1"), DummyContainer("T2")]
|
||||
|
||||
with pytest.raises(ValueError, match="Unsupported transfer mode"):
|
||||
run(
|
||||
lh.transfer_liquid(
|
||||
sources=sources,
|
||||
targets=targets,
|
||||
tip_racks=[],
|
||||
use_channels=[0],
|
||||
asp_vols=[1, 1],
|
||||
dis_vols=[1, 1, 1],
|
||||
mix_times=0,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_mix_single_target_produces_matching_cycles():
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
target = DummyContainer("T_mix")
|
||||
|
||||
run(lh.mix(targets=[target], mix_time=2, mix_vol=5))
|
||||
|
||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
||||
dispenses = [payload for name, payload in lh.calls if name == "dispense"]
|
||||
assert len(aspirates) == len(dispenses) == 2
|
||||
assert all(call["resources"] == [target] for call in aspirates)
|
||||
assert all(call["vols"] == [5] for call in aspirates)
|
||||
assert all(call["resources"] == [target] for call in dispenses)
|
||||
assert all(call["vols"] == [5] for call in dispenses)
|
||||
|
||||
|
||||
def test_mix_multiple_targets_supports_per_target_offsets():
|
||||
lh = FakeLiquidHandler(channel_num=1)
|
||||
targets = [DummyContainer("T0"), DummyContainer("T1")]
|
||||
offsets = ["left", "right"]
|
||||
heights = [0.1, 0.2]
|
||||
rates = [0.5, 1.0]
|
||||
|
||||
run(
|
||||
lh.mix(
|
||||
targets=targets,
|
||||
mix_time=1,
|
||||
mix_vol=3,
|
||||
offsets=offsets,
|
||||
height_to_bottom=heights,
|
||||
mix_rate=rates,
|
||||
)
|
||||
)
|
||||
|
||||
aspirates = [payload for name, payload in lh.calls if name == "aspirate"]
|
||||
assert len(aspirates) == 2
|
||||
assert aspirates[0]["resources"] == [targets[0]]
|
||||
assert aspirates[0]["offsets"] == [offsets[0]]
|
||||
assert aspirates[0]["liquid_height"] == [heights[0]]
|
||||
assert aspirates[0]["flow_rates"] == [rates[0]]
|
||||
assert aspirates[1]["resources"] == [targets[1]]
|
||||
assert aspirates[1]["offsets"] == [offsets[1]]
|
||||
assert aspirates[1]["liquid_height"] == [heights[1]]
|
||||
assert aspirates[1]["flow_rates"] == [rates[1]]
|
||||
|
||||
|
||||
def test_set_tiprack_per_type_resumes_first_physical_rack():
|
||||
"""同型号多次 set_tiprack 时接续第一盒剩余孔位,而非从新盒 A1 开始。"""
|
||||
from pylabrobot.liquid_handling import LiquidHandlerChatterboxBackend
|
||||
from pylabrobot.resources import Deck, Tip, TipRack, TipSpot, create_equally_spaced
|
||||
|
||||
mk = lambda: Tip(
|
||||
has_filter=False, total_tip_length=10.0, maximal_volume=300.0, fitting_depth=2.0
|
||||
)
|
||||
|
||||
class TipTypeAlpha(TipRack):
|
||||
pass
|
||||
|
||||
class TipTypeBeta(TipRack):
|
||||
pass
|
||||
|
||||
def make_rack(cls: type, name: str) -> TipRack:
|
||||
items = create_equally_spaced(
|
||||
TipSpot,
|
||||
num_items_x=12,
|
||||
num_items_y=2,
|
||||
dx=0,
|
||||
dy=0,
|
||||
dz=0,
|
||||
item_dx=9,
|
||||
item_dy=9,
|
||||
size_x=1,
|
||||
size_y=1,
|
||||
make_tip=mk,
|
||||
)
|
||||
return cls(name, 120, 40, 10, items=items)
|
||||
|
||||
rack1 = make_rack(TipTypeAlpha, "rack_phys_1")
|
||||
rack2 = make_rack(TipTypeBeta, "rack_phys_2")
|
||||
rack3 = make_rack(TipTypeAlpha, "rack_phys_3")
|
||||
|
||||
lh = LiquidHandlerAbstract(
|
||||
LiquidHandlerChatterboxBackend(1), Deck(), channel_num=1, simulator=False
|
||||
)
|
||||
flat1 = lh._flatten_tips_from_one(rack1)
|
||||
assert len(flat1) == 24
|
||||
|
||||
lh.set_tiprack([rack1])
|
||||
for i in range(12):
|
||||
assert lh._get_next_tip() is flat1[i]
|
||||
|
||||
lh.set_tiprack([rack2])
|
||||
spot_b = lh._get_next_tip()
|
||||
assert "rack_phys_2" in spot_b.name
|
||||
|
||||
lh.set_tiprack([rack3])
|
||||
spot_resume = lh._get_next_tip()
|
||||
assert spot_resume is flat1[12], "第三次同型号应接续 rack1 第二行首孔,而非 rack3 首孔"
|
||||
assert spot_resume is not lh._flatten_tips_from_one(rack3)[0]
|
||||
|
||||
|
||||
137
tests/resources/test_resource_tracker_history.py
Normal file
137
tests/resources/test_resource_tracker_history.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""P9 — ``_augment_states_with_liquid_history`` 单元测试(OS→Cloud sync 链路 Phase C)。
|
||||
|
||||
详见 ``product_designs/protocol_convert/09-liquid-history-unknown-debug.md`` §6.3 / §8 T4。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pytest
|
||||
|
||||
from unilabos.resources.resource_tracker import _augment_states_with_liquid_history
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures:纯 dataclass 模拟 PLR 资源树(避免引入 PLR 真实实例化)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeTracker:
|
||||
liquid_history: Any = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FakeResource:
|
||||
name: str
|
||||
tracker: Any = None
|
||||
children: List["FakeResource"] = field(default_factory=list)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAugmentStatesWithLiquidHistory:
|
||||
def test_single_well_history_attached(self) -> None:
|
||||
well = FakeResource("well_A1", tracker=FakeTracker(liquid_history=[
|
||||
{"name": "Plasma", "volume": 100, "action": "set"}
|
||||
]))
|
||||
states: Dict[str, Any] = {"well_A1": {"liquids": [], "pending_liquids": []}}
|
||||
|
||||
_augment_states_with_liquid_history(well, states)
|
||||
|
||||
assert "liquid_history" in states["well_A1"]
|
||||
assert states["well_A1"]["liquid_history"] == [
|
||||
{"name": "Plasma", "volume": 100, "action": "set"}
|
||||
]
|
||||
|
||||
def test_recursive_walk_attaches_to_all_wells(self) -> None:
|
||||
"""resource 树有多层时,每个有 tracker 的节点都会被并入 states。"""
|
||||
wells = [
|
||||
FakeResource(f"well_{i}", tracker=FakeTracker(liquid_history=[
|
||||
{"name": f"L_{i}", "volume": i * 10, "action": "set"}
|
||||
]))
|
||||
for i in range(3)
|
||||
]
|
||||
plate = FakeResource("plate", children=wells)
|
||||
deck = FakeResource("deck", children=[plate])
|
||||
states: Dict[str, Any] = {
|
||||
"deck": {"liquids": []},
|
||||
"plate": {"liquids": []},
|
||||
"well_0": {"liquids": []},
|
||||
"well_1": {"liquids": []},
|
||||
"well_2": {"liquids": []},
|
||||
}
|
||||
|
||||
_augment_states_with_liquid_history(deck, states)
|
||||
|
||||
assert states["well_0"]["liquid_history"] == [{"name": "L_0", "volume": 0, "action": "set"}]
|
||||
assert states["well_1"]["liquid_history"] == [{"name": "L_1", "volume": 10, "action": "set"}]
|
||||
assert states["well_2"]["liquid_history"] == [{"name": "L_2", "volume": 20, "action": "set"}]
|
||||
|
||||
def test_no_tracker_node_skipped(self) -> None:
|
||||
"""没有 tracker 的节点(如 deck 自身)跳过,state dict 不被污染。"""
|
||||
deck = FakeResource("deck") # tracker=None
|
||||
states: Dict[str, Any] = {"deck": {"some_field": 1}}
|
||||
|
||||
_augment_states_with_liquid_history(deck, states)
|
||||
|
||||
assert "liquid_history" not in states["deck"]
|
||||
|
||||
def test_existing_liquid_history_in_state_not_overwritten(self) -> None:
|
||||
"""state 已经有 liquid_history 字段(例如 PLR 升级未来支持了)→ 不覆盖。"""
|
||||
well = FakeResource("well_A1", tracker=FakeTracker(liquid_history=[
|
||||
{"name": "Plasma", "volume": 100, "action": "set"}
|
||||
]))
|
||||
states: Dict[str, Any] = {"well_A1": {"liquid_history": ["preexisting"]}}
|
||||
|
||||
_augment_states_with_liquid_history(well, states)
|
||||
|
||||
assert states["well_A1"]["liquid_history"] == ["preexisting"]
|
||||
|
||||
def test_history_is_shallow_copied(self) -> None:
|
||||
"""augment 后的 history 应是独立 list(避免运行时 mutate 污染 dump 结果)。"""
|
||||
original_history = [{"name": "X", "volume": 1, "action": "set"}]
|
||||
well = FakeResource("well_A1", tracker=FakeTracker(liquid_history=original_history))
|
||||
states: Dict[str, Any] = {"well_A1": {}}
|
||||
|
||||
_augment_states_with_liquid_history(well, states)
|
||||
|
||||
# mutate runtime history 不应反映到 augmented state
|
||||
original_history.append({"name": "Y", "volume": 2, "action": "set"})
|
||||
assert len(states["well_A1"]["liquid_history"]) == 1
|
||||
|
||||
def test_node_not_in_states_silently_skipped(self) -> None:
|
||||
"""resource 树中的节点 name 不在 ``states`` 字典里 → 静默跳过。"""
|
||||
well = FakeResource("well_orphan", tracker=FakeTracker(liquid_history=[
|
||||
{"name": "X", "volume": 1, "action": "set"}
|
||||
]))
|
||||
states: Dict[str, Any] = {"well_A1": {}}
|
||||
|
||||
_augment_states_with_liquid_history(well, states)
|
||||
|
||||
# 不应该新增 well_orphan 键,也不应污染 well_A1
|
||||
assert "well_orphan" not in states
|
||||
assert "liquid_history" not in states["well_A1"]
|
||||
|
||||
def test_non_list_liquid_history_skipped(self) -> None:
|
||||
"""tracker.liquid_history 非 list 时(异常情况)→ 跳过,不写入 state。"""
|
||||
well = FakeResource("well_A1", tracker=FakeTracker(liquid_history="broken"))
|
||||
states: Dict[str, Any] = {"well_A1": {}}
|
||||
|
||||
_augment_states_with_liquid_history(well, states)
|
||||
|
||||
assert "liquid_history" not in states["well_A1"]
|
||||
|
||||
def test_empty_history_still_written(self) -> None:
|
||||
"""tracker.liquid_history = [] 是合法状态 → 应写入空 list(表示"未有任何液体操作")。"""
|
||||
well = FakeResource("well_A1", tracker=FakeTracker(liquid_history=[]))
|
||||
states: Dict[str, Any] = {"well_A1": {}}
|
||||
|
||||
_augment_states_with_liquid_history(well, states)
|
||||
|
||||
assert states["well_A1"]["liquid_history"] == []
|
||||
351
tests/workflow/test_build_protocol_graph_target_device.py
Normal file
351
tests/workflow/test_build_protocol_graph_target_device.py
Normal file
@@ -0,0 +1,351 @@
|
||||
"""P6.1 / P6.1.1 `build_protocol_graph` 集成测试 —— 对应 06-labware-mapping-table.md §11.7.7 C / §11.8.7 C。
|
||||
|
||||
6 条用例:
|
||||
|
||||
- `test_build_graph_default_target_device_prcxi` —— 不传 target_device 时默认 "prcxi",
|
||||
与 P6 等价(PRCXI_* class_name)。
|
||||
- `test_build_graph_explicit_target_device_prcxi` —— 显式 "prcxi" 与默认完全等价。
|
||||
- `test_build_graph_target_device_unknown_falls_back_to_default_section` —— 未声明的
|
||||
target_device 由 loader 自动 fallback 到 ``target_devices.default``;第一版 default
|
||||
段按 prcxi 拷贝,所以结果应与 "prcxi" 完全一致。
|
||||
- `test_build_graph_per_device_tip_class` —— 临时 YAML 同时声明 prcxi 与 beckman tip
|
||||
量程档;同一 transfer_liquid 在 target_device="prcxi" / "beckman" 下命中不同 class。
|
||||
- `test_field_renamed_target_class_name` —— `labware_info` 写入的字段是
|
||||
`target_class_name`,**旧字段 `prcxi_class_name` 不存在**。
|
||||
- `test_build_graph_model_level_slot_remap` —— P6.1.1:``target_model`` 透传到
|
||||
``_map_deck_slot`` 后改变 create_resource 的 slot(同厂商不同型号 deck 物理布局不同)。
|
||||
|
||||
本测试在导入 common.py 之前 mock 掉 matplotlib / networkx.drawing.nx_agraph,避免在
|
||||
没有图形依赖的最小 Python 环境下也能跑(与 P6 批量回归脚本同样的策略)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
|
||||
def _install_fake_optional_deps() -> None:
|
||||
"""安装 matplotlib / networkx.drawing.nx_agraph 的 fake 实现,避免本地环境硬依赖。
|
||||
|
||||
common.py 在模块级 import 这些库做可视化辅助;build_protocol_graph 主路径不会真用到。
|
||||
fake 模块只需要满足 ``from X import Y`` 的查找即可。
|
||||
"""
|
||||
if "matplotlib" not in sys.modules:
|
||||
fake_matplotlib = types.ModuleType("matplotlib")
|
||||
sys.modules["matplotlib"] = fake_matplotlib
|
||||
if "matplotlib.pyplot" not in sys.modules:
|
||||
fake_plt = types.ModuleType("matplotlib.pyplot")
|
||||
sys.modules["matplotlib.pyplot"] = fake_plt
|
||||
# networkx.drawing.nx_agraph.to_agraph 依赖 pygraphviz;不可用时给个空 stub
|
||||
try:
|
||||
from networkx.drawing import nx_agraph # noqa: F401
|
||||
except Exception:
|
||||
nx_drawing = types.ModuleType("networkx.drawing")
|
||||
nx_agraph_mod = types.ModuleType("networkx.drawing.nx_agraph")
|
||||
|
||||
def _to_agraph(_g): # type: ignore[no-untyped-def]
|
||||
raise RuntimeError("nx_agraph fake — not used in build_protocol_graph main path")
|
||||
|
||||
nx_agraph_mod.to_agraph = _to_agraph # type: ignore[attr-defined]
|
||||
nx_drawing.nx_agraph = nx_agraph_mod # type: ignore[attr-defined]
|
||||
sys.modules["networkx.drawing"] = nx_drawing
|
||||
sys.modules["networkx.drawing.nx_agraph"] = nx_agraph_mod
|
||||
|
||||
|
||||
_install_fake_optional_deps()
|
||||
|
||||
from unilabos.workflow import labware_mapping as lm # noqa: E402
|
||||
from unilabos.workflow.common import build_protocol_graph # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_mapping_cache():
|
||||
"""每个用例后清 lru_cache,避免跨用例污染。"""
|
||||
yield
|
||||
lm.reload_mapping()
|
||||
|
||||
|
||||
# ==================== 公共 fixture:最小 transfer_liquid 协议 ====================
|
||||
|
||||
|
||||
def _minimal_labware_info() -> dict:
|
||||
"""返回最小可用的 labware_info(mutable,每个 case 独立 build 一份)。
|
||||
|
||||
包含 tip rack + 24-tube rack + 96 wellplate(slot 1/2/3),覆盖 P6.1 主要 kind。
|
||||
tube rack / plate 显式声明 ``num_wells``,避免在无 labware_defs / 无 prcxi_labware 模板
|
||||
时通过 well-count 启发式(well_n=3)误判孔数;与真实协议中 labware_defs 提供 num_wells
|
||||
的行为对齐。
|
||||
"""
|
||||
return {
|
||||
"tips": {
|
||||
"slot": 1,
|
||||
"well": [],
|
||||
"labware": "opentrons_96_tiprack_300ul",
|
||||
"object": "tiprack",
|
||||
},
|
||||
"samples": {
|
||||
"slot": 2,
|
||||
"well": ["A1", "A2", "A3"],
|
||||
"labware": "opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap",
|
||||
"object": "source",
|
||||
"num_wells": 24,
|
||||
},
|
||||
"plate_target": {
|
||||
"slot": 3,
|
||||
"well": ["A1", "A2", "A3"],
|
||||
"labware": "opentrons_96_wellplate_300ul_pcr",
|
||||
"object": "target",
|
||||
"num_wells": 96,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _minimal_protocol_steps() -> list:
|
||||
"""最小 transfer_liquid 协议步骤:asp_vols/dis_vols 最大 200 µL → PRCXI 300ul 档。"""
|
||||
return [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "samples",
|
||||
"targets": "plate_target",
|
||||
"tip_racks": "tips",
|
||||
"asp_vols": [200.0, 200.0, 200.0],
|
||||
"dis_vols": [200.0, 200.0, 200.0],
|
||||
},
|
||||
"step_number": 1,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def _collect_create_resource_classes(graph) -> dict:
|
||||
"""从工作流图中提取每个 create_resource 节点的 ``slot_on_deck → class_name``。"""
|
||||
out: dict = {}
|
||||
for _nid, node in graph.nodes.items():
|
||||
if node.get("template_name") != "create_resource":
|
||||
continue
|
||||
param = node.get("param") or {}
|
||||
slot = str(param.get("slot_on_deck") or "")
|
||||
cls = str(param.get("class_name") or "")
|
||||
if slot:
|
||||
out[slot] = cls
|
||||
return out
|
||||
|
||||
|
||||
# ==================== 5 条核心用例 ====================
|
||||
|
||||
|
||||
def test_build_graph_default_target_device_prcxi():
|
||||
"""不传 target_device → 默认 "prcxi" → 与 P6 等价(PRCXI_* class_name)。"""
|
||||
labware_info = _minimal_labware_info()
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware_info,
|
||||
protocol_steps=_minimal_protocol_steps(),
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
classes = _collect_create_resource_classes(g)
|
||||
assert classes["1"] == "PRCXI_300ul_Tips" # 200 µL → 300 档
|
||||
assert classes["2"] == "PRCXI_EP_Adapter" # 24-tube rack
|
||||
assert classes["3"] == "PRCXI_BioER_96_wellplate" # 96 wellplate
|
||||
|
||||
|
||||
def test_build_graph_explicit_target_device_prcxi():
|
||||
"""显式传 target_device="prcxi" 应与默认完全等价。"""
|
||||
labware_info_a = _minimal_labware_info()
|
||||
labware_info_b = _minimal_labware_info()
|
||||
g_default = build_protocol_graph(
|
||||
labware_info=labware_info_a,
|
||||
protocol_steps=_minimal_protocol_steps(),
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
g_prcxi = build_protocol_graph(
|
||||
labware_info=labware_info_b,
|
||||
protocol_steps=_minimal_protocol_steps(),
|
||||
workstation_name="PRCXI",
|
||||
target_device="prcxi",
|
||||
)
|
||||
assert _collect_create_resource_classes(g_default) == _collect_create_resource_classes(g_prcxi)
|
||||
|
||||
|
||||
def test_build_graph_target_device_unknown_falls_back_to_default_section():
|
||||
"""未声明的 target_device → loader 自动 fallback 到固定段 target_devices.default + warning。
|
||||
|
||||
第一版 default 段按 prcxi 拷贝填充 → 结果应与 target_device="prcxi" 完全等价(PRCXI_*)。
|
||||
"""
|
||||
labware_info_a = _minimal_labware_info()
|
||||
labware_info_b = _minimal_labware_info()
|
||||
g_prcxi = build_protocol_graph(
|
||||
labware_info=labware_info_a,
|
||||
protocol_steps=_minimal_protocol_steps(),
|
||||
workstation_name="PRCXI",
|
||||
target_device="prcxi",
|
||||
)
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
g_unknown = build_protocol_graph(
|
||||
labware_info=labware_info_b,
|
||||
protocol_steps=_minimal_protocol_steps(),
|
||||
workstation_name="PRCXI",
|
||||
target_device="unknown_xxx",
|
||||
)
|
||||
assert _collect_create_resource_classes(g_unknown) == _collect_create_resource_classes(g_prcxi)
|
||||
# loader 至少打 1 次 warning 提示「未声明、已回退到 default」
|
||||
assert any(
|
||||
("未在 labware_mapping.yaml" in str(w.message))
|
||||
or ("target_devices.default" in str(w.message))
|
||||
for w in caught
|
||||
)
|
||||
|
||||
|
||||
def test_build_graph_per_device_tip_class(tmp_path, monkeypatch):
|
||||
"""同一 protocol,target_device="prcxi" / "beckman" 在 200µL 下命中不同 tip 档(P6.1.1 schema)。"""
|
||||
yaml_path = tmp_path / "labware_mapping.yaml"
|
||||
yaml_path.write_text(
|
||||
'kinds:\n'
|
||||
' - {pattern: "trash", kind: trash}\n'
|
||||
' - {pattern: "tiprack|tip[_ ]?rack|opentrons_\\\\d+_tiprack", kind: tip_rack}\n'
|
||||
' - {pattern: "tuberack|tube[_ ]rack|eppendorf.*rack|safelock.*rack", kind: tube_rack}\n'
|
||||
' - {pattern: ".*", kind: plate}\n'
|
||||
'target_devices:\n'
|
||||
' default:\n'
|
||||
' slot_remap: {default: {"4": "13", "8": "14"}, by_object: {trash: {"12": "16"}}}\n'
|
||||
' rules:\n'
|
||||
' - {kind: tip_rack, hole_count: 96, volume_max: 10, class_name: PRCXI_10uL_Tips}\n'
|
||||
' - {kind: tip_rack, hole_count: 96, volume_max: 299.9, class_name: PRCXI_300ul_Tips}\n'
|
||||
' - {kind: tip_rack, hole_count: 96, class_name: PRCXI_1000uL_Tips}\n'
|
||||
' - {kind: tube_rack, hole_count: 24, class_name: PRCXI_EP_Adapter}\n'
|
||||
' - {kind: plate, hole_count: 96, class_name: PRCXI_BioER_96_wellplate}\n'
|
||||
' prcxi:\n'
|
||||
' slot_remap: {default: {"4": "13", "8": "14"}, by_object: {trash: {"12": "16"}}}\n'
|
||||
' rules:\n'
|
||||
' - {kind: tip_rack, hole_count: 96, volume_max: 10, class_name: PRCXI_10uL_Tips}\n'
|
||||
' - {kind: tip_rack, hole_count: 96, volume_max: 299.9, class_name: PRCXI_300ul_Tips}\n'
|
||||
' - {kind: tip_rack, hole_count: 96, class_name: PRCXI_1000uL_Tips}\n'
|
||||
' - {kind: tube_rack, hole_count: 24, class_name: PRCXI_EP_Adapter}\n'
|
||||
' - {kind: plate, hole_count: 96, class_name: PRCXI_BioER_96_wellplate}\n'
|
||||
' beckman:\n'
|
||||
' slot_remap: {default: {"4": "13"}, by_object: {trash: {"12": "16"}}}\n'
|
||||
' rules:\n'
|
||||
' - {kind: tip_rack, hole_count: 96, volume_max: 20, class_name: Beckman_20uL_Tips}\n'
|
||||
' - {kind: tip_rack, hole_count: 96, volume_max: 199.9, class_name: Beckman_200uL_Tips}\n'
|
||||
' - {kind: tip_rack, hole_count: 96, class_name: Beckman_1000uL_Tips}\n'
|
||||
' - {kind: tube_rack, hole_count: 24, class_name: Beckman_24_TubeRack}\n'
|
||||
' - {kind: plate, hole_count: 96, class_name: Beckman_BioMek_96_wellplate}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(lm, "_DEFAULT_PATH", yaml_path)
|
||||
lm.reload_mapping()
|
||||
|
||||
g_prcxi = build_protocol_graph(
|
||||
labware_info=_minimal_labware_info(),
|
||||
protocol_steps=_minimal_protocol_steps(),
|
||||
workstation_name="PRCXI",
|
||||
target_device="prcxi",
|
||||
)
|
||||
g_beckman = build_protocol_graph(
|
||||
labware_info=_minimal_labware_info(),
|
||||
protocol_steps=_minimal_protocol_steps(),
|
||||
workstation_name="PRCXI",
|
||||
target_device="beckman",
|
||||
)
|
||||
|
||||
classes_prcxi = _collect_create_resource_classes(g_prcxi)
|
||||
classes_beckman = _collect_create_resource_classes(g_beckman)
|
||||
|
||||
# 200 µL:prcxi 走 300 档;beckman 200 档已超 → 1000 档
|
||||
assert classes_prcxi["1"] == "PRCXI_300ul_Tips"
|
||||
assert classes_beckman["1"] == "Beckman_1000uL_Tips"
|
||||
# plate / tube rack 也按 target_device 输出对应厂商类
|
||||
assert classes_prcxi["2"] == "PRCXI_EP_Adapter"
|
||||
assert classes_beckman["2"] == "Beckman_24_TubeRack"
|
||||
assert classes_prcxi["3"] == "PRCXI_BioER_96_wellplate"
|
||||
assert classes_beckman["3"] == "Beckman_BioMek_96_wellplate"
|
||||
|
||||
|
||||
def test_field_renamed_target_class_name():
|
||||
"""`labware_info` 写入的字段是 `target_class_name`;旧字段 `prcxi_class_name` 不存在。"""
|
||||
labware_info = _minimal_labware_info()
|
||||
build_protocol_graph(
|
||||
labware_info=labware_info,
|
||||
protocol_steps=_minimal_protocol_steps(),
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
for lid, item in labware_info.items():
|
||||
assert "target_class_name" in item, f"{lid!r} 缺少 target_class_name 字段"
|
||||
assert "prcxi_class_name" not in item, f"{lid!r} 残留了旧字段 prcxi_class_name"
|
||||
assert item["target_class_name"], f"{lid!r} target_class_name 为空"
|
||||
|
||||
|
||||
# ==================== P6.1.1 新增集成测试 ====================
|
||||
|
||||
|
||||
def _labware_info_slot4_plate() -> dict:
|
||||
"""slot=4 的 96 板:用来验证 target_model 透传后 slot_remap 改变 create_resource 的槽位。"""
|
||||
return {
|
||||
"plate_slot4": {
|
||||
"slot": 4,
|
||||
"well": ["A1"],
|
||||
"labware": "opentrons_96_wellplate_300ul_pcr",
|
||||
"object": "target",
|
||||
"num_wells": 96,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_build_graph_model_level_slot_remap(tmp_path, monkeypatch):
|
||||
"""P6.1.1:target_model 透传到 _map_deck_slot 后改变 create_resource 的 slot_on_deck。
|
||||
|
||||
YAML 中 prcxi 厂商级 slot_remap 4→13;模型 "4040" 显式覆盖 4→16。
|
||||
同一份 labware_info(slot=4)build 出的两份图,slot_on_deck 应分别为 "13" 与 "16"。
|
||||
"""
|
||||
yaml_path = tmp_path / "labware_mapping.yaml"
|
||||
yaml_path.write_text(
|
||||
'kinds: [{pattern: ".*", kind: plate}]\n'
|
||||
'target_devices:\n'
|
||||
' default:\n'
|
||||
' slot_remap: {default: {"4": "13"}, by_object: {}}\n'
|
||||
' rules: [{kind: plate, hole_count: 96, class_name: PRCXI_BioER_96_wellplate}]\n'
|
||||
' prcxi:\n'
|
||||
' slot_remap: {default: {"4": "13"}, by_object: {}}\n'
|
||||
' rules: [{kind: plate, hole_count: 96, class_name: PRCXI_BioER_96_wellplate}]\n'
|
||||
' models:\n'
|
||||
' "4040":\n'
|
||||
' slot_remap: {default: {"4": "16"}, by_object: {}}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(lm, "_DEFAULT_PATH", yaml_path)
|
||||
lm.reload_mapping()
|
||||
|
||||
g_default = build_protocol_graph(
|
||||
labware_info=_labware_info_slot4_plate(),
|
||||
protocol_steps=[],
|
||||
workstation_name="PRCXI",
|
||||
target_device="prcxi",
|
||||
)
|
||||
g_model_4040 = build_protocol_graph(
|
||||
labware_info=_labware_info_slot4_plate(),
|
||||
protocol_steps=[],
|
||||
workstation_name="PRCXI",
|
||||
target_device="prcxi",
|
||||
target_model="4040",
|
||||
)
|
||||
|
||||
classes_default = _collect_create_resource_classes(g_default)
|
||||
classes_4040 = _collect_create_resource_classes(g_model_4040)
|
||||
|
||||
# 厂商级(无 model)→ slot 4 → "13"
|
||||
assert "13" in classes_default, f"未找到 slot 13,实际生成的 slots: {list(classes_default)}"
|
||||
assert "16" not in classes_default
|
||||
# 模型 4040 → slot 4 → "16"
|
||||
assert "16" in classes_4040, f"未找到 slot 16,实际生成的 slots: {list(classes_4040)}"
|
||||
assert "13" not in classes_4040
|
||||
# class_name 不变(rules 继承厂商级)
|
||||
assert classes_default["13"] == "PRCXI_BioER_96_wellplate"
|
||||
assert classes_4040["16"] == "PRCXI_BioER_96_wellplate"
|
||||
369
tests/workflow/test_common_cross_slot_v2.py
Normal file
369
tests/workflow/test_common_cross_slot_v2.py
Normal file
@@ -0,0 +1,369 @@
|
||||
"""P2 v2 跨 slot transfer_liquid 合并 —— Stage 3 (`workflow/common.py`) 集成测试。
|
||||
|
||||
对应 ``product_designs/protocol_convert/02-cross-slot-merge.md`` §9.5 step 6.2。
|
||||
|
||||
v2 设计要点(与本测试用例的映射)
|
||||
-----------------------------------
|
||||
当 transfer_liquid 节点 ``params.targets`` 是 ``list[str]`` 时,``build_protocol_graph``
|
||||
在该 transfer_liquid 之前**插入一个 merged ``set_liquid_from_plate`` 节点**:
|
||||
|
||||
- merged 节点的 ``param.wells`` 是按 ``params.targets`` 顺序通过 cursor 拼出来的有序跨板
|
||||
well refs(每个元素是 ``{id, name, parent: reagent_key, type: "well"}``)。
|
||||
- merged 节点接收来自每个涉及 plate 的 ``create_resource`` 节点的多入边
|
||||
(``labware`` → ``wells_identifier``)。
|
||||
- merged 节点的 ``output_wells`` 通过**单条边**连到 transfer_liquid 的 ``targets_identifier``。
|
||||
- transfer_liquid 节点的 ``params.targets`` 被改写为 synthetic key
|
||||
``_merged_targets_<idx>``(runtime 不消费 list 形态),保证 INPUT_PORT_MAPPING 走单边路径。
|
||||
|
||||
用例
|
||||
----
|
||||
- ``test_emit_merged_set_liquid_basic`` — 4 个 distinct reagent_key(51b9a5 主场景)。
|
||||
- ``test_emit_merged_set_liquid_repeat_key`` — 同 reagent_key 重复(同板多孔)。
|
||||
- ``test_emit_merged_set_liquid_mixed`` — 跨板混合 + 同板重复(cursor 推进)。
|
||||
- ``test_emit_merged_set_liquid_8ch`` — 与 P1 multi-channel 复合(8 通道 cross-slot)。
|
||||
- ``test_transfer_liquid_targets_rewrite`` — transfer_liquid 节点改写后只剩 1 条
|
||||
``targets_identifier`` 入边;params.targets 不再是 list。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
|
||||
def _install_fake_optional_deps() -> None:
|
||||
"""与 test_build_protocol_graph_target_device.py 一致的可选依赖 stub。"""
|
||||
if "matplotlib" not in sys.modules:
|
||||
sys.modules["matplotlib"] = types.ModuleType("matplotlib")
|
||||
if "matplotlib.pyplot" not in sys.modules:
|
||||
sys.modules["matplotlib.pyplot"] = types.ModuleType("matplotlib.pyplot")
|
||||
try:
|
||||
from networkx.drawing import nx_agraph # noqa: F401
|
||||
except Exception:
|
||||
nx_drawing = types.ModuleType("networkx.drawing")
|
||||
nx_agraph_mod = types.ModuleType("networkx.drawing.nx_agraph")
|
||||
nx_agraph_mod.to_agraph = lambda _g: None # type: ignore[attr-defined]
|
||||
nx_drawing.nx_agraph = nx_agraph_mod # type: ignore[attr-defined]
|
||||
sys.modules["networkx.drawing"] = nx_drawing
|
||||
sys.modules["networkx.drawing.nx_agraph"] = nx_agraph_mod
|
||||
|
||||
|
||||
_install_fake_optional_deps()
|
||||
|
||||
import pytest # noqa: E402
|
||||
|
||||
from unilabos.workflow.common import build_protocol_graph # noqa: E402
|
||||
|
||||
|
||||
# ==================== 测试辅助:从工作流图中提取节点/边 ====================
|
||||
|
||||
|
||||
def _nodes_by_template(graph, template_name: str) -> List[Dict[str, Any]]:
|
||||
return [
|
||||
{"id": nid, **node}
|
||||
for nid, node in graph.nodes.items()
|
||||
if node.get("template_name") == template_name
|
||||
]
|
||||
|
||||
|
||||
def _create_resource_by_slot(graph) -> Dict[str, str]:
|
||||
"""slot_on_deck (str) -> create_resource 节点 ID。"""
|
||||
out: Dict[str, str] = {}
|
||||
for nid, node in graph.nodes.items():
|
||||
if node.get("template_name") == "create_resource":
|
||||
slot = str(node.get("param", {}).get("slot_on_deck") or "")
|
||||
if slot:
|
||||
out[slot] = nid
|
||||
return out
|
||||
|
||||
|
||||
def _edges_to(graph, target_id: str) -> List[Dict[str, Any]]:
|
||||
return [e for e in graph.edges if e["target"] == target_id]
|
||||
|
||||
|
||||
def _edges_from(graph, source_id: str) -> List[Dict[str, Any]]:
|
||||
return [e for e in graph.edges if e["source"] == source_id]
|
||||
|
||||
|
||||
# ==================== fixture:构造跨板 labware + steps ====================
|
||||
|
||||
|
||||
def _cross_slot_labware_info() -> Dict[str, Dict[str, Any]]:
|
||||
"""51b9a5 简化:slot1 source + slot2/3/5/6 target plates + slot12 tip。"""
|
||||
return {
|
||||
"l1": {
|
||||
"slot": 1,
|
||||
"well": ["A1"],
|
||||
"labware": "nest_12_reservoir_15ml",
|
||||
"object": "source",
|
||||
},
|
||||
"plate_slot2": {
|
||||
"slot": 2,
|
||||
"well": ["A1"],
|
||||
"labware": "nest_96_wellplate_2ml_deep",
|
||||
"object": "target",
|
||||
},
|
||||
"plate_slot3": {
|
||||
"slot": 3,
|
||||
"well": ["A1"],
|
||||
"labware": "nest_96_wellplate_2ml_deep",
|
||||
"object": "target",
|
||||
},
|
||||
"plate_slot5": {
|
||||
"slot": 5,
|
||||
"well": ["A1"],
|
||||
"labware": "nest_96_wellplate_2ml_deep",
|
||||
"object": "target",
|
||||
},
|
||||
"plate_slot6": {
|
||||
"slot": 6,
|
||||
"well": ["A1"],
|
||||
"labware": "nest_96_wellplate_2ml_deep",
|
||||
"object": "target",
|
||||
},
|
||||
"tiprack_12": {
|
||||
"slot": 12,
|
||||
"well": [],
|
||||
"labware": "opentrons_96_tiprack_300ul",
|
||||
"object": "tiprack",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _cross_slot_protocol_steps(targets: List[str], dis_vols: List[float]) -> List[Dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "l1",
|
||||
"targets": targets,
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": dis_vols.copy(),
|
||||
"dis_vols": dis_vols.copy(),
|
||||
},
|
||||
"step_number": 1,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# ==================== 用例 ====================
|
||||
|
||||
|
||||
def test_emit_merged_set_liquid_basic():
|
||||
"""51b9a5 主场景:targets=[A,B,C,D] → 1 merged set_liquid 节点
|
||||
+ 4 条入边(来自 4 个 distinct create_resource)+ 1 条出边(去 transfer_liquid)。
|
||||
"""
|
||||
targets = ["plate_slot2", "plate_slot3", "plate_slot5", "plate_slot6"]
|
||||
dis_vols = [8.3, 8.3, 8.3, 8.3]
|
||||
g = build_protocol_graph(
|
||||
labware_info=_cross_slot_labware_info(),
|
||||
protocol_steps=_cross_slot_protocol_steps(targets, dis_vols),
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
set_liquid_nodes = _nodes_by_template(g, "set_liquid_from_plate")
|
||||
merged_nodes = [n for n in set_liquid_nodes if str(n.get("name", "")).startswith("_merged_targets_")]
|
||||
assert len(merged_nodes) == 1, (
|
||||
f"应有且仅有 1 个 merged set_liquid_from_plate 节点(v2 跨板聚合器);"
|
||||
f" 实际找到 {len(merged_nodes)}: {[n.get('name') for n in merged_nodes]}"
|
||||
)
|
||||
merged = merged_nodes[0]
|
||||
merged_id = merged["id"]
|
||||
|
||||
# param.wells:长度 4,每元素的 parent 是对应 reagent_key
|
||||
wells = merged.get("param", {}).get("wells") or []
|
||||
assert len(wells) == 4
|
||||
assert [w["parent"] for w in wells] == targets, "merged.wells 顺序必须严格按 targets 列表"
|
||||
# well 字段映射到 reagent.well[0](都是 "A1")
|
||||
for w, key in zip(wells, targets):
|
||||
assert w["id"].endswith("/A1"), f"well id 应包含 well 名: {w}"
|
||||
assert w["parent"] == key
|
||||
|
||||
# 入边:4 条来自 distinct create_resource 节点(slot 2/3/5/6),target_port=wells_identifier
|
||||
cr_by_slot = _create_resource_by_slot(g)
|
||||
in_edges = _edges_to(g, merged_id)
|
||||
in_sources = {e["source"] for e in in_edges if e.get("target_handle_key") == "wells_identifier"}
|
||||
expected_sources = {cr_by_slot[s] for s in ("2", "3", "5", "6")}
|
||||
assert in_sources == expected_sources, (
|
||||
f"merged 节点应接收 4 个 distinct create_resource 的 wells_identifier 边;"
|
||||
f" 实际 {in_sources} vs 期望 {expected_sources}"
|
||||
)
|
||||
|
||||
# 出边:1 条到 transfer_liquid(targets_identifier)
|
||||
transfer_nodes = _nodes_by_template(g, "transfer_liquid")
|
||||
assert len(transfer_nodes) == 1
|
||||
transfer_id = transfer_nodes[0]["id"]
|
||||
out_to_transfer = [
|
||||
e for e in _edges_from(g, merged_id)
|
||||
if e["target"] == transfer_id and e.get("target_handle_key") == "targets_identifier"
|
||||
]
|
||||
assert len(out_to_transfer) == 1, (
|
||||
f"merged 节点应向 transfer_liquid.targets_identifier 发出唯一 1 条边;"
|
||||
f" 实际 {len(out_to_transfer)}"
|
||||
)
|
||||
|
||||
|
||||
def test_emit_merged_set_liquid_repeat_key():
|
||||
"""同 reagent_key 重复(同板多孔):targets=[A,A,A] + reagent.A.well=[A1,A2,A3]
|
||||
→ merged.wells 顺序 = [A/A1, A/A2, A/A3](cursor 推进取每个 well)。
|
||||
"""
|
||||
labware = _cross_slot_labware_info()
|
||||
labware["plate_slot2"]["well"] = ["A1", "A2", "A3"]
|
||||
|
||||
targets = ["plate_slot2", "plate_slot2", "plate_slot2"]
|
||||
dis_vols = [10.0, 20.0, 30.0]
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=_cross_slot_protocol_steps(targets, dis_vols),
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
merged_nodes = [
|
||||
n for n in _nodes_by_template(g, "set_liquid_from_plate")
|
||||
if str(n.get("name", "")).startswith("_merged_targets_")
|
||||
]
|
||||
assert len(merged_nodes) == 1
|
||||
wells = merged_nodes[0]["param"]["wells"]
|
||||
assert [w["id"].rsplit("/", 1)[-1] for w in wells] == ["A1", "A2", "A3"], (
|
||||
"cursor 应依次取 reagent.A.well[0/1/2]"
|
||||
)
|
||||
assert all(w["parent"] == "plate_slot2" for w in wells)
|
||||
|
||||
|
||||
def test_emit_merged_set_liquid_mixed():
|
||||
"""跨板 + 同板重复:targets=[A,B,A,C] + reagent.A.well=[A1,A2]
|
||||
→ merged.wells = [A/A1, B/A1, A/A2, C/A1]。
|
||||
"""
|
||||
labware = _cross_slot_labware_info()
|
||||
labware["plate_slot2"]["well"] = ["A1", "A2"]
|
||||
|
||||
targets = ["plate_slot2", "plate_slot3", "plate_slot2", "plate_slot5"]
|
||||
dis_vols = [10.0, 20.0, 30.0, 40.0]
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=_cross_slot_protocol_steps(targets, dis_vols),
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
merged_nodes = [
|
||||
n for n in _nodes_by_template(g, "set_liquid_from_plate")
|
||||
if str(n.get("name", "")).startswith("_merged_targets_")
|
||||
]
|
||||
assert len(merged_nodes) == 1
|
||||
wells = merged_nodes[0]["param"]["wells"]
|
||||
ids = [(w["parent"], w["id"].rsplit("/", 1)[-1]) for w in wells]
|
||||
assert ids == [
|
||||
("plate_slot2", "A1"),
|
||||
("plate_slot3", "A1"),
|
||||
("plate_slot2", "A2"),
|
||||
("plate_slot5", "A1"),
|
||||
]
|
||||
|
||||
|
||||
def test_emit_merged_set_liquid_8ch():
|
||||
"""与 P1 multi-channel 复合:targets=[A]*8+[B]*8(每列 8 通道)。
|
||||
|
||||
merged.wells 长度 16,前 8 全 plate_slot2 的 8 个 well,后 8 全 plate_slot3 的 8 个 well。
|
||||
"""
|
||||
labware = _cross_slot_labware_info()
|
||||
# 8 通道场景 reagent.well 已被 P1 multi 展开为长度 8
|
||||
labware["plate_slot2"]["well"] = [f"{r}1" for r in "ABCDEFGH"]
|
||||
labware["plate_slot3"]["well"] = [f"{r}1" for r in "ABCDEFGH"]
|
||||
|
||||
targets = ["plate_slot2"] * 8 + ["plate_slot3"] * 8
|
||||
dis_vols = [5.0] * 16
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=_cross_slot_protocol_steps(targets, dis_vols),
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
merged_nodes = [
|
||||
n for n in _nodes_by_template(g, "set_liquid_from_plate")
|
||||
if str(n.get("name", "")).startswith("_merged_targets_")
|
||||
]
|
||||
assert len(merged_nodes) == 1
|
||||
wells = merged_nodes[0]["param"]["wells"]
|
||||
assert len(wells) == 16
|
||||
# 前 8 全 plate_slot2,后 8 全 plate_slot3(满足 cross-slot × 8ch 列对齐约束)
|
||||
assert all(w["parent"] == "plate_slot2" for w in wells[:8])
|
||||
assert all(w["parent"] == "plate_slot3" for w in wells[8:])
|
||||
# well 名顺序:A1..H1 重复两遍
|
||||
assert [w["id"].rsplit("/", 1)[-1] for w in wells[:8]] == [f"{r}1" for r in "ABCDEFGH"]
|
||||
assert [w["id"].rsplit("/", 1)[-1] for w in wells[8:]] == [f"{r}1" for r in "ABCDEFGH"]
|
||||
|
||||
|
||||
def test_transfer_liquid_targets_rewrite():
|
||||
"""transfer_liquid 节点改写后只剩 1 条 targets_identifier 入边;params.targets 不再是 list。"""
|
||||
targets = ["plate_slot2", "plate_slot3", "plate_slot5", "plate_slot6"]
|
||||
dis_vols = [8.3, 8.3, 8.3, 8.3]
|
||||
g = build_protocol_graph(
|
||||
labware_info=_cross_slot_labware_info(),
|
||||
protocol_steps=_cross_slot_protocol_steps(targets, dis_vols),
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
transfer_nodes = _nodes_by_template(g, "transfer_liquid")
|
||||
assert len(transfer_nodes) == 1
|
||||
tnode = transfer_nodes[0]
|
||||
transfer_id = tnode["id"]
|
||||
|
||||
# params.targets:v2 中 list 形态在 INPUT_PORT_MAPPING 处理后被清空([])或为单字符串
|
||||
# (不再是原始 list[str]——避免下游 runtime 对其再做无序聚合)
|
||||
tparams = tnode.get("param", {}) or {}
|
||||
assert not isinstance(tparams.get("targets"), list) or tparams.get("targets") == [], (
|
||||
f"v2:params.targets 不再是非空 list;实际 {tparams.get('targets')!r}"
|
||||
)
|
||||
|
||||
# targets_identifier 端口:只有 1 条入边
|
||||
in_targets_edges = [
|
||||
e for e in _edges_to(g, transfer_id)
|
||||
if e.get("target_handle_key") == "targets_identifier"
|
||||
]
|
||||
assert len(in_targets_edges) == 1, (
|
||||
f"v2:transfer_liquid.targets_identifier 必须是单入边(来自 merged set_liquid);"
|
||||
f" 实际 {len(in_targets_edges)}"
|
||||
)
|
||||
|
||||
# 这条入边的源端口必须是 output_wells
|
||||
edge = in_targets_edges[0]
|
||||
assert edge.get("source_handle_key") == "output_wells"
|
||||
|
||||
|
||||
def test_str_targets_no_merged_node_emitted():
|
||||
"""对照组:targets 为 str(单 reagent) → 不插入 merged set_liquid_from_plate 节点。
|
||||
|
||||
保证 v2 改造**只**对 list 形态触发,单 reagent 走 P3 原有 per-plate set_liquid 路径。
|
||||
"""
|
||||
labware = _cross_slot_labware_info()
|
||||
labware["plate_slot2"]["well"] = ["A1", "A2", "A3"]
|
||||
|
||||
steps = [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "l1",
|
||||
"targets": "plate_slot2", # ← 单 str,非 list
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [8.3, 8.3, 8.3],
|
||||
"dis_vols": [8.3, 8.3, 8.3],
|
||||
},
|
||||
"step_number": 1,
|
||||
}
|
||||
]
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=steps,
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
merged_nodes = [
|
||||
n for n in _nodes_by_template(g, "set_liquid_from_plate")
|
||||
if str(n.get("name", "")).startswith("_merged_targets_")
|
||||
]
|
||||
assert merged_nodes == [], "str 形态 targets 不应触发 v2 merged 聚合节点"
|
||||
452
tests/workflow/test_common_liquid_name_from_reagent.py
Normal file
452
tests/workflow/test_common_liquid_name_from_reagent.py
Normal file
@@ -0,0 +1,452 @@
|
||||
"""P8 — Stage 3 (``workflow/common.py``) 写入 ``set_liquid_from_plate.param.liquid_names`` 时
|
||||
优先取 ``reagent[key].liquid_name``,缺省时 fallback 到 reagent_key。
|
||||
|
||||
对应 ``product_designs/protocol_convert/08-liquid-name-from-reagent-block.md`` §3.4 + §5。
|
||||
|
||||
设计要点
|
||||
--------
|
||||
- ``reagent[key].liquid_name`` 是 P8 新增的**可选**字段,承载真实化学名(与 reagent_key
|
||||
解耦:reagent_key 仍是数据流引用名 / 业务别名,``liquid_name`` 是写入 PLR tracker /
|
||||
前端的 human-readable 名称)。
|
||||
- ``liquid_name`` 来源优先级:Stage 0 mock ``Well.load_liquid(liquid=...)`` 实参 >
|
||||
README 语义词 > 不写(Stage 3 fallback 到 reagent_key)。
|
||||
- ``liquid_name`` 保留空格 / 中文 / 括号等原字符,**不**做 snake_case / underscore 替换。
|
||||
- 旧 JSON(无 ``liquid_name`` 字段)行为完全不变(设计点 §7.A)。
|
||||
|
||||
测试用例
|
||||
--------
|
||||
- ``test_per_plate_fallback_when_no_liquid_name`` —— 缺省 fallback:
|
||||
reagent 块无 ``liquid_name`` → liquid_names[i] == reagent_key(与 P8 前一致)。
|
||||
- ``test_per_plate_uses_explicit_liquid_name`` —— 显式 liquid_name:
|
||||
liquid_names[i] == "EDTA Plasma"。
|
||||
- ``test_per_plate_preserves_spaces_and_special_chars`` —— 含空格 / 括号:
|
||||
liquid_names[i] 不被 ``replace(" ", "_")`` 处理(不同于 reagent_key 用的 res_id)。
|
||||
- ``test_merged_node_uses_explicit_liquid_name_per_dispense`` —— merged 节点
|
||||
每个 dispense 独立取 ``liquid_name or key``,部分有部分无能共存。
|
||||
- ``test_liquid_name_independent_of_reagent_key_normalization`` —— 与 P4 共存:
|
||||
reagent_key 仍是 ``samples_2`` 等去重后缀,但 liquid_names 写的是真实化学名。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
|
||||
def _install_fake_optional_deps() -> None:
|
||||
"""与 test_common_set_liquid_dedup.py 一致的可选依赖 stub。"""
|
||||
if "matplotlib" not in sys.modules:
|
||||
sys.modules["matplotlib"] = types.ModuleType("matplotlib")
|
||||
if "matplotlib.pyplot" not in sys.modules:
|
||||
sys.modules["matplotlib.pyplot"] = types.ModuleType("matplotlib.pyplot")
|
||||
try:
|
||||
from networkx.drawing import nx_agraph # noqa: F401
|
||||
except Exception:
|
||||
nx_drawing = types.ModuleType("networkx.drawing")
|
||||
nx_agraph_mod = types.ModuleType("networkx.drawing.nx_agraph")
|
||||
nx_agraph_mod.to_agraph = lambda _g: None # type: ignore[attr-defined]
|
||||
nx_drawing.nx_agraph = nx_agraph_mod # type: ignore[attr-defined]
|
||||
sys.modules["networkx.drawing"] = nx_drawing
|
||||
sys.modules["networkx.drawing.nx_agraph"] = nx_agraph_mod
|
||||
|
||||
|
||||
_install_fake_optional_deps()
|
||||
|
||||
import pytest # noqa: E402
|
||||
|
||||
from unilabos.workflow.common import build_protocol_graph # noqa: E402
|
||||
|
||||
|
||||
# ==================== 辅助 ====================
|
||||
|
||||
|
||||
def _set_liquid_nodes(graph) -> List[Dict[str, Any]]:
|
||||
return [
|
||||
{"id": nid, **node}
|
||||
for nid, node in graph.nodes.items()
|
||||
if node.get("template_name") == "set_liquid_from_plate"
|
||||
]
|
||||
|
||||
|
||||
def _per_plate_for(graph, reagent_key: str) -> Dict[str, Any]:
|
||||
"""根据 ``description = "Set liquid: <reagent_key>"`` 反查 per-plate 节点。"""
|
||||
for n in _set_liquid_nodes(graph):
|
||||
if n.get("description") == f"Set liquid: {reagent_key}":
|
||||
return n
|
||||
raise AssertionError(f"未找到 per-plate set_liquid_from_plate(reagent_key={reagent_key!r})")
|
||||
|
||||
|
||||
def _merged_nodes(graph) -> List[Dict[str, Any]]:
|
||||
return [
|
||||
n for n in _set_liquid_nodes(graph)
|
||||
if str(n.get("name", "")).startswith("_merged_targets_")
|
||||
]
|
||||
|
||||
|
||||
def _make_source_target_labware(
|
||||
*,
|
||||
source_key: str = "src_1",
|
||||
source_liquid_name: str | None = None,
|
||||
target_keys: List[str] | None = None,
|
||||
target_liquid_names: Dict[str, str] | None = None,
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
"""构造 1 个 source + N 个 target reagent + 1 个 tip rack。
|
||||
|
||||
``*_liquid_name`` 为 None / 缺省时**不**写入 ``liquid_name`` 字段,
|
||||
模拟旧 schema / mock 未给 liquid_name 的真实回归场景。
|
||||
"""
|
||||
info: Dict[str, Dict[str, Any]] = {}
|
||||
source_entry: Dict[str, Any] = {
|
||||
"slot": 1,
|
||||
"well": ["A1"],
|
||||
"labware": "nest_12_reservoir_15ml",
|
||||
"object": "source",
|
||||
}
|
||||
if source_liquid_name is not None:
|
||||
source_entry["liquid_name"] = source_liquid_name
|
||||
info[source_key] = source_entry
|
||||
|
||||
target_keys = target_keys or ["t_A"]
|
||||
target_liquid_names = target_liquid_names or {}
|
||||
for i, tk in enumerate(target_keys, start=1):
|
||||
entry: Dict[str, Any] = {
|
||||
"slot": 2 + i,
|
||||
"well": ["A1"],
|
||||
"labware": "nest_96_wellplate_2ml_deep",
|
||||
"object": "target",
|
||||
}
|
||||
if tk in target_liquid_names:
|
||||
entry["liquid_name"] = target_liquid_names[tk]
|
||||
info[tk] = entry
|
||||
|
||||
info["tiprack_12"] = {
|
||||
"slot": 12,
|
||||
"well": [],
|
||||
"labware": "opentrons_96_tiprack_300ul",
|
||||
"object": "tiprack",
|
||||
}
|
||||
return info
|
||||
|
||||
|
||||
# ==================== T1 缺省 fallback ====================
|
||||
|
||||
|
||||
def test_per_plate_fallback_when_no_liquid_name():
|
||||
"""reagent block 无 ``liquid_name`` 字段 → liquid_names[i] == reagent_key(P8 前行为)。"""
|
||||
labware = _make_source_target_labware(
|
||||
source_key="src_1",
|
||||
target_keys=["t_A"],
|
||||
# 都不给 liquid_name
|
||||
)
|
||||
steps = [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_1",
|
||||
"targets": "t_A",
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [10.0],
|
||||
"dis_vols": [10.0],
|
||||
},
|
||||
"step_number": 1,
|
||||
}
|
||||
]
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=steps,
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
src_node = _per_plate_for(g, "src_1")
|
||||
tgt_node = _per_plate_for(g, "t_A")
|
||||
assert src_node["param"]["liquid_names"] == ["src_1"], (
|
||||
f"无 liquid_name 时 source per-plate 应 fallback 到 reagent_key;"
|
||||
f" 实际 {src_node['param']['liquid_names']}"
|
||||
)
|
||||
assert tgt_node["param"]["liquid_names"] == ["t_A"], (
|
||||
f"无 liquid_name 时 target per-plate 应 fallback 到 reagent_key;"
|
||||
f" 实际 {tgt_node['param']['liquid_names']}"
|
||||
)
|
||||
|
||||
|
||||
# ==================== T2 显式 liquid_name ====================
|
||||
|
||||
|
||||
def test_per_plate_uses_explicit_liquid_name():
|
||||
"""reagent block 含 ``liquid_name`` → liquid_names[i] 用该值(不是 reagent_key)。"""
|
||||
labware = _make_source_target_labware(
|
||||
source_key="src_1",
|
||||
source_liquid_name="EDTA Plasma",
|
||||
target_keys=["t_A"],
|
||||
target_liquid_names={"t_A": "PBS Diluent"},
|
||||
)
|
||||
steps = [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_1",
|
||||
"targets": "t_A",
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [10.0],
|
||||
"dis_vols": [10.0],
|
||||
},
|
||||
"step_number": 1,
|
||||
}
|
||||
]
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=steps,
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
src_node = _per_plate_for(g, "src_1")
|
||||
tgt_node = _per_plate_for(g, "t_A")
|
||||
assert src_node["param"]["liquid_names"] == ["EDTA Plasma"], (
|
||||
f"source per-plate 应使用 reagent.liquid_name;实际 {src_node['param']['liquid_names']}"
|
||||
)
|
||||
assert tgt_node["param"]["liquid_names"] == ["PBS Diluent"], (
|
||||
f"target per-plate 应使用 reagent.liquid_name;实际 {tgt_node['param']['liquid_names']}"
|
||||
)
|
||||
|
||||
|
||||
# ==================== T3 空格 / 括号 ====================
|
||||
|
||||
|
||||
def test_per_plate_preserves_spaces_and_special_chars():
|
||||
"""``liquid_name`` 保留空格 / 括号 / 中文等原字符,不被 replace(' ', '_') 处理。
|
||||
|
||||
这条与 reagent_key 走 ``res_id = str(labware_id).replace(' ', '_')`` 的语义不同。
|
||||
"""
|
||||
labware = _make_source_target_labware(
|
||||
source_key="src_1",
|
||||
source_liquid_name="Tris HCl pH 8.0 (1×)",
|
||||
target_keys=["t_A"],
|
||||
target_liquid_names={"t_A": "稀释液 A"},
|
||||
)
|
||||
steps = [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_1",
|
||||
"targets": "t_A",
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [10.0],
|
||||
"dis_vols": [10.0],
|
||||
},
|
||||
"step_number": 1,
|
||||
}
|
||||
]
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=steps,
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
src_node = _per_plate_for(g, "src_1")
|
||||
tgt_node = _per_plate_for(g, "t_A")
|
||||
|
||||
assert src_node["param"]["liquid_names"] == ["Tris HCl pH 8.0 (1×)"], (
|
||||
f"空格 / 括号应原样保留;实际 {src_node['param']['liquid_names']}"
|
||||
)
|
||||
assert tgt_node["param"]["liquid_names"] == ["稀释液 A"], (
|
||||
f"中文应原样保留;实际 {tgt_node['param']['liquid_names']}"
|
||||
)
|
||||
|
||||
# reagent_key 自身仍受 ``res_id = replace(' ', '_')`` 影响,
|
||||
# 但本测试 reagent_key 不含空格,故 sl_node_title 仍以 reagent_key 为根。
|
||||
# 这里仅断言 liquid_names 字段独立于 reagent_key normalize。
|
||||
|
||||
|
||||
# ==================== T4 merged 节点跨板部分有部分无 ====================
|
||||
|
||||
|
||||
def test_merged_node_uses_explicit_liquid_name_per_dispense():
|
||||
"""merged 节点 ``liquid_names`` 与 list-targets 同长,每个元素独立取
|
||||
``reagent[key].liquid_name or key``:本例 3 个 target,2 个有显式名、1 个无。
|
||||
"""
|
||||
labware = _make_source_target_labware(
|
||||
source_key="src_1",
|
||||
target_keys=["t_A", "t_B", "t_C"],
|
||||
target_liquid_names={
|
||||
"t_A": "Plasma",
|
||||
# t_B 无 liquid_name
|
||||
"t_C": "Buffer X",
|
||||
},
|
||||
)
|
||||
steps = [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_1",
|
||||
"targets": ["t_A", "t_B", "t_C"],
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [5.0] * 3,
|
||||
"dis_vols": [5.0] * 3,
|
||||
},
|
||||
"step_number": 1,
|
||||
}
|
||||
]
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=steps,
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
merged = _merged_nodes(g)
|
||||
assert len(merged) == 1, f"应有 1 个 merged 节点,实际 {len(merged)}"
|
||||
liquid_names = merged[0]["param"]["liquid_names"]
|
||||
assert liquid_names == ["Plasma", "t_B", "Buffer X"], (
|
||||
f"merged 每 dispense 独立取 liquid_name or key;实际 {liquid_names}"
|
||||
)
|
||||
|
||||
|
||||
# ==================== T5 与 P4 reagent_key 后缀共存 ====================
|
||||
|
||||
|
||||
def test_liquid_name_independent_of_reagent_key_normalization():
|
||||
"""P4 命名链产生 ``samples_2`` 这种带后缀的 reagent_key(跨板去重);
|
||||
P8 ``liquid_name`` 应保持原始化学名,**不**带 P4 的去重后缀。
|
||||
|
||||
构造:2 个 target reagent_keys ``samples`` / ``samples_2``(不同 slot,
|
||||
模拟跨板同液体被 Stage 2 去重),都标 liquid_name="Bacterial Culture"。
|
||||
"""
|
||||
labware = _make_source_target_labware(
|
||||
source_key="src_1",
|
||||
target_keys=["samples", "samples_2"],
|
||||
target_liquid_names={
|
||||
"samples": "Bacterial Culture",
|
||||
"samples_2": "Bacterial Culture",
|
||||
},
|
||||
)
|
||||
steps = [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_1",
|
||||
"targets": ["samples", "samples_2"],
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [5.0, 5.0],
|
||||
"dis_vols": [5.0, 5.0],
|
||||
},
|
||||
"step_number": 1,
|
||||
}
|
||||
]
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=steps,
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
merged = _merged_nodes(g)
|
||||
assert len(merged) == 1
|
||||
liquid_names = merged[0]["param"]["liquid_names"]
|
||||
assert liquid_names == ["Bacterial Culture", "Bacterial Culture"], (
|
||||
f"P8 liquid_name 应与 P4 reagent_key 后缀解耦:同液体的两个 reagent_key 应得相同"
|
||||
f" liquid_name;实际 {liquid_names}"
|
||||
)
|
||||
# 同时 reagent_key 仍是 samples / samples_2(不变)
|
||||
wells = merged[0]["param"]["wells"]
|
||||
parents = [w["parent"] for w in wells]
|
||||
assert parents == ["samples", "samples_2"], (
|
||||
f"merged wells.parent 应等于 list-targets reagent_keys;实际 {parents}"
|
||||
)
|
||||
|
||||
|
||||
# ==================== T6 source per-plate / target per-plate 同步生效 ====================
|
||||
|
||||
|
||||
def test_both_source_and_target_per_plate_use_liquid_name():
|
||||
"""str-targets 路径(无 merged)下,source 和 target 都走 per-plate emit,
|
||||
各自独立取 ``liquid_name``。"""
|
||||
labware = _make_source_target_labware(
|
||||
source_key="src_1",
|
||||
source_liquid_name="Reagent A",
|
||||
target_keys=["t_A"],
|
||||
target_liquid_names={"t_A": "Reagent B"},
|
||||
)
|
||||
steps = [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_1",
|
||||
"targets": "t_A", # str-targets,不触发 merged
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [10.0],
|
||||
"dis_vols": [10.0],
|
||||
},
|
||||
"step_number": 1,
|
||||
}
|
||||
]
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=steps,
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
assert _merged_nodes(g) == [], "str-targets 不应产生 merged 节点"
|
||||
src_node = _per_plate_for(g, "src_1")
|
||||
tgt_node = _per_plate_for(g, "t_A")
|
||||
assert src_node["param"]["liquid_names"] == ["Reagent A"]
|
||||
assert tgt_node["param"]["liquid_names"] == ["Reagent B"]
|
||||
|
||||
|
||||
# ==================== T7 多孔同 reagent → 整列 liquid_names 一致 ====================
|
||||
|
||||
|
||||
def test_multi_well_reagent_replicates_liquid_name():
|
||||
"""1 个 reagent 含 8 wells(multi-channel 扩展场景)→ liquid_names 应是
|
||||
``[liquid_name] * 8``,与 wells 长度一致。"""
|
||||
labware: Dict[str, Dict[str, Any]] = {
|
||||
"src_1": {
|
||||
"slot": 1,
|
||||
"well": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"],
|
||||
"labware": "nest_96_wellplate_100ul_pcr_full_skirt",
|
||||
"object": "source",
|
||||
"liquid_name": "Mastermix",
|
||||
},
|
||||
"t_A": {
|
||||
"slot": 3,
|
||||
"well": ["A1"],
|
||||
"labware": "nest_96_wellplate_2ml_deep",
|
||||
"object": "target",
|
||||
},
|
||||
"tiprack_12": {
|
||||
"slot": 12,
|
||||
"well": [],
|
||||
"labware": "opentrons_96_tiprack_300ul",
|
||||
"object": "tiprack",
|
||||
},
|
||||
}
|
||||
steps = [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_1",
|
||||
"targets": "t_A",
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [10.0],
|
||||
"dis_vols": [10.0],
|
||||
},
|
||||
"step_number": 1,
|
||||
}
|
||||
]
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=steps,
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
src_node = _per_plate_for(g, "src_1")
|
||||
liquid_names = src_node["param"]["liquid_names"]
|
||||
assert liquid_names == ["Mastermix"] * 8, (
|
||||
f"per-plate 应把 liquid_name 复制 well_count 份;实际 {liquid_names}"
|
||||
)
|
||||
# 同时 wells / volumes 长度一致
|
||||
assert len(src_node["param"]["wells"]) == 8
|
||||
assert len(src_node["param"]["volumes"]) == 8
|
||||
174
tests/workflow/test_common_plate_num_children_hint.py
Normal file
174
tests/workflow/test_common_plate_num_children_hint.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""P6 §17 hint bug —— `_infer_plate_num_children_from_labware_hint` 误把
|
||||
reagent_id 末尾数字(如 ``samples_6`` 的 ``_6``)当作孔板规格,导致
|
||||
``_apply_target_labware_class_auto_match`` fallback 到 PRCXI 4-孔 trough 模板。
|
||||
|
||||
跨板 fix(P2 v2 §14)把 plate name 作为 prefix 编码进 ``well_names`` 之后,
|
||||
runtime 调用 ``plate.get_well("A5")`` 严格定位 well,trough plate 上不存在
|
||||
``A5`` 会直接 IndexError,使得这个隐藏多年的孔数推断 bug 浮出。
|
||||
|
||||
修复策略(方案 A)
|
||||
-----
|
||||
hint 只用 ``item.get("labware", "")``,**不再**拼上 ``labware_id``(reagent_key
|
||||
是业务名,不应参与孔板规格推断)。
|
||||
|
||||
测试矩阵
|
||||
----
|
||||
- ``test_reagent_key_numeric_suffix_must_not_match_hint`` —— samples_6 / samples_24 /
|
||||
samples_96 + nunc_rectangular_agar_plate → hint 返回 None(labware string 不带孔数信息)。
|
||||
- ``test_labware_string_X_well_correctly_inferred`` —— labware="nest_96_wellplate..." → 96;
|
||||
"custom_384_wellplate" → 384;"nest_24_wellplate_2ml_pcr" → 24。
|
||||
- ``test_apply_does_not_classify_samples_6_as_trough`` —— 集成:构造 Agar Plating-like
|
||||
reagent block(slot 8 上 12 个 samples_X,X 末尾含 6/24/96),跑
|
||||
``_apply_target_labware_class_auto_match`` 后,samples_6/24 不再得到 trough class。
|
||||
- ``test_real_labware_96_wellplate_still_inferred_via_labware_str`` —— 即便 labware_id
|
||||
与孔数无关,``nest_96_wellplate_100ul_pcr_full_skirt`` 这种 labware 命名仍应被识别为 96。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
|
||||
def _install_fake_optional_deps() -> None:
|
||||
if "matplotlib" not in sys.modules:
|
||||
sys.modules["matplotlib"] = types.ModuleType("matplotlib")
|
||||
if "matplotlib.pyplot" not in sys.modules:
|
||||
sys.modules["matplotlib.pyplot"] = types.ModuleType("matplotlib.pyplot")
|
||||
|
||||
|
||||
_install_fake_optional_deps()
|
||||
|
||||
import pytest # noqa: E402
|
||||
|
||||
from unilabos.workflow.common import ( # noqa: E402
|
||||
_apply_target_labware_class_auto_match,
|
||||
_infer_plate_num_children_from_labware_hint,
|
||||
_reconcile_slot_carrier_target_class,
|
||||
)
|
||||
|
||||
|
||||
# ==================== unit:hint 函数本身 ====================
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"labware_id",
|
||||
["samples_6", "samples_24", "samples_96", "samples_12", "samples_48"],
|
||||
)
|
||||
def test_reagent_key_numeric_suffix_must_not_match_hint(labware_id):
|
||||
"""reagent_id 末尾的孔数关键字数字不应被识别为孔板规格。"""
|
||||
item = {
|
||||
"slot": 8,
|
||||
"well": ["A5"],
|
||||
"labware": "nunc_rectangular_agar_plate",
|
||||
"object": "target",
|
||||
}
|
||||
assert _infer_plate_num_children_from_labware_hint(labware_id, item) is None, (
|
||||
f"reagent_id {labware_id!r} 不应被识别为孔板规格 "
|
||||
f"(其末尾数字应当被忽略;labware string 不含 96/384/etc 关键字)"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"labware_str,expected",
|
||||
[
|
||||
("nest_96_wellplate_100ul_pcr_full_skirt", 96),
|
||||
("custom_384_wellplate", 384),
|
||||
("nest_24_wellplate_2ml_pcr", 24),
|
||||
("custom_48_wellplate", 48),
|
||||
("opentrons_12_wellplate_15ml", 12),
|
||||
("nest_6_wellplate_5ml", 6),
|
||||
("nunc_rectangular_agar_plate", None),
|
||||
("", None),
|
||||
],
|
||||
)
|
||||
def test_labware_string_well_count_inferred(labware_str, expected):
|
||||
item = {"labware": labware_str}
|
||||
assert (
|
||||
_infer_plate_num_children_from_labware_hint("samples", item) == expected
|
||||
), f"labware {labware_str!r} 应推断为 {expected!r}"
|
||||
|
||||
|
||||
# ==================== integration:模拟 Agar Plating ====================
|
||||
|
||||
|
||||
def _agar_plating_reagent_block():
|
||||
"""反推自 unilabos_data/req_workflow_upload.json:12 列 × 9 reagent per step。
|
||||
|
||||
slot 8 (mapped 14) 上 12 个 reagent_keys: samples_6, samples_15, samples_24,
|
||||
samples_33, samples_42, samples_51, samples_60, samples_69, samples_78,
|
||||
samples_87, samples_96, samples_105.
|
||||
"""
|
||||
info = {}
|
||||
slot_for_idx = {0: 3, 1: 4, 2: 5, 3: 6, 4: 7, 5: 8, 6: 9, 7: 10, 8: 11}
|
||||
cols = [f"A{i + 1}" for i in range(12)]
|
||||
for col_i, col in enumerate(cols):
|
||||
for di in range(9):
|
||||
n = col_i * 9 + di + 1
|
||||
key = "samples" if n == 1 else f"samples_{n}"
|
||||
info[key] = {
|
||||
"slot": slot_for_idx[di],
|
||||
"well": [col],
|
||||
"labware": "nunc_rectangular_agar_plate",
|
||||
"object": "target",
|
||||
}
|
||||
for i in range(12):
|
||||
key = "sources" if i == 0 else f"sources_{i + 1}"
|
||||
info[key] = {
|
||||
"slot": 2,
|
||||
"well": [cols[i]],
|
||||
"labware": "nest_96_wellplate_100ul_pcr_full_skirt",
|
||||
"object": "source",
|
||||
}
|
||||
info["tiprack_1"] = {
|
||||
"slot": 1,
|
||||
"well": None,
|
||||
"labware": "opentrons_96_tiprack_10ul",
|
||||
"object": "tiprack",
|
||||
}
|
||||
info["trash"] = {
|
||||
"slot": 12,
|
||||
"well": None,
|
||||
"labware": "opentrons_1_trash_1100ml_fixed",
|
||||
"object": "trash",
|
||||
}
|
||||
return info
|
||||
|
||||
|
||||
def test_apply_does_not_classify_samples_6_as_trough():
|
||||
"""集成回归:Agar Plating-like reagent block 跑完类匹配 + slot 统一后,
|
||||
slot 8 上 12 个 reagent 不应得到 4-孔 trough class。"""
|
||||
info = _agar_plating_reagent_block()
|
||||
_apply_target_labware_class_auto_match(
|
||||
info, preserve_tip_rack_incoming_class=True, target_device="prcxi"
|
||||
)
|
||||
_reconcile_slot_carrier_target_class(
|
||||
info, preserve_tip_rack_incoming_class=True, target_device="prcxi"
|
||||
)
|
||||
slot8_keys = [
|
||||
"samples_6", "samples_15", "samples_24", "samples_33",
|
||||
"samples_42", "samples_51", "samples_60", "samples_69",
|
||||
"samples_78", "samples_87", "samples_96", "samples_105",
|
||||
]
|
||||
for k in slot8_keys:
|
||||
cls = info[k].get("target_class_name") or ""
|
||||
assert "trough" not in cls.lower(), (
|
||||
f"reagent {k} 被误识别为 trough class: {cls!r};"
|
||||
"这通常是 hint 误把 reagent_id 末尾数字当孔板规格"
|
||||
)
|
||||
|
||||
|
||||
def test_real_labware_96_wellplate_still_inferred_via_labware_str():
|
||||
"""labware string 含 96_wellplate 时应该正常识别为 96,不被 fix 破坏。"""
|
||||
item = {
|
||||
"slot": 2,
|
||||
"well": ["A1"],
|
||||
"labware": "nest_96_wellplate_100ul_pcr_full_skirt",
|
||||
"object": "source",
|
||||
}
|
||||
assert _infer_plate_num_children_from_labware_hint("sources", item) == 96
|
||||
379
tests/workflow/test_common_set_liquid_dedup.py
Normal file
379
tests/workflow/test_common_set_liquid_dedup.py
Normal file
@@ -0,0 +1,379 @@
|
||||
"""P2 v2 §14 set_liquid_from_plate 去重 —— Stage 3 (`workflow/common.py`) 集成测试。
|
||||
|
||||
对应 ``product_designs/protocol_convert/02-cross-slot-merge.md`` §14(2026-05-22 plan)。
|
||||
|
||||
§14 设计要点
|
||||
-----------------
|
||||
当 ``transfer_liquid.params.targets`` 是 ``list[str]`` 时,``_emit_merged_set_liquid``
|
||||
已经为该 transfer 插入一个 merged ``set_liquid_from_plate`` 节点,
|
||||
其 ``param.wells`` 聚合了 list 中所有 reagent_keys 的跨板 wells。
|
||||
|
||||
§14 之前:第二步循环(``for labware_id, item in labware_info.items()``)仍然为
|
||||
list-targets 中出现的每个 reagent_key 创建一个 per-plate ``set_liquid_from_plate`` 节点,
|
||||
导致**节点冗余**(per-plate 节点的 ``output_wells`` 对 transfer_liquid 的
|
||||
``targets_identifier`` 边毫无贡献 —— transfer_liquid 单边只接 merged 节点)。
|
||||
|
||||
§14 改造:在第二步循环**之前**预扫描 protocol_steps,收集
|
||||
``set_liquid_covered_by_merged: Set[str]``(出现在某个 list[str] targets 中的所有 keys)
|
||||
与 ``set_liquid_referenced_by_str: Set[str]``(出现在 str targets 中的所有 keys)。
|
||||
循环内对 ``object="target"`` 且 ``key ∈ covered ∧ key ∉ referenced_by_str`` 的 reagent_key
|
||||
**跳过** per-plate 节点创建。
|
||||
|
||||
测试用例
|
||||
----
|
||||
- ``test_per_plate_skipped_when_covered_by_merged`` —— list-targets 覆盖的
|
||||
target reagent_keys 不再产生 per-plate set_liquid_from_plate。
|
||||
- ``test_per_plate_kept_when_also_referenced_by_str_targets`` —— R1 缓解:
|
||||
同时被 list-targets 和 str-targets 引用的 reagent_key 仍保留 per-plate。
|
||||
- ``test_str_targets_protocol_unaffected`` —— 单 slot 协议(仅 str-targets)
|
||||
节点数完全不变(回归防护)。
|
||||
- ``test_51b9a5_style_node_count`` —— 12 list-targets × len=9 大规模场景:
|
||||
set_liquid_from_plate 总节点数 = source per-plate + merged + 0 target per-plate。
|
||||
- ``test_source_per_plate_always_kept`` —— source 端不受 §14 影响:source
|
||||
reagent_keys 不出现在 targets 字段中,per-plate 节点恒在。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
|
||||
def _install_fake_optional_deps() -> None:
|
||||
"""与 test_common_cross_slot_v2.py 一致的可选依赖 stub。"""
|
||||
if "matplotlib" not in sys.modules:
|
||||
sys.modules["matplotlib"] = types.ModuleType("matplotlib")
|
||||
if "matplotlib.pyplot" not in sys.modules:
|
||||
sys.modules["matplotlib.pyplot"] = types.ModuleType("matplotlib.pyplot")
|
||||
try:
|
||||
from networkx.drawing import nx_agraph # noqa: F401
|
||||
except Exception:
|
||||
nx_drawing = types.ModuleType("networkx.drawing")
|
||||
nx_agraph_mod = types.ModuleType("networkx.drawing.nx_agraph")
|
||||
nx_agraph_mod.to_agraph = lambda _g: None # type: ignore[attr-defined]
|
||||
nx_drawing.nx_agraph = nx_agraph_mod # type: ignore[attr-defined]
|
||||
sys.modules["networkx.drawing"] = nx_drawing
|
||||
sys.modules["networkx.drawing.nx_agraph"] = nx_agraph_mod
|
||||
|
||||
|
||||
_install_fake_optional_deps()
|
||||
|
||||
import pytest # noqa: E402
|
||||
|
||||
from unilabos.workflow.common import build_protocol_graph # noqa: E402
|
||||
|
||||
|
||||
# ==================== 辅助 ====================
|
||||
|
||||
|
||||
def _nodes_by_template(graph, template_name: str) -> List[Dict[str, Any]]:
|
||||
return [
|
||||
{"id": nid, **node}
|
||||
for nid, node in graph.nodes.items()
|
||||
if node.get("template_name") == template_name
|
||||
]
|
||||
|
||||
|
||||
def _set_liquid_nodes_split(graph):
|
||||
"""返回 (per_plate_nodes, merged_nodes)。merged 节点 name 以 `_merged_targets_` 开头。"""
|
||||
all_sl = _nodes_by_template(graph, "set_liquid_from_plate")
|
||||
merged = [n for n in all_sl if str(n.get("name", "")).startswith("_merged_targets_")]
|
||||
per_plate = [n for n in all_sl if not str(n.get("name", "")).startswith("_merged_targets_")]
|
||||
return per_plate, merged
|
||||
|
||||
|
||||
def _labware_with_targets(target_keys: List[str], source_keys: List[str] | None = None) -> Dict[str, Dict[str, Any]]:
|
||||
"""构造 labware_info:source 端 1 个 + 任意数量 target plates + tip rack。"""
|
||||
info: Dict[str, Dict[str, Any]] = {}
|
||||
source_keys = source_keys or ["src_1"]
|
||||
for i, sk in enumerate(source_keys, start=1):
|
||||
info[sk] = {
|
||||
"slot": 1 + i - 1, # slot 1 占位(实际可能映射)
|
||||
"well": ["A1"],
|
||||
"labware": "nest_12_reservoir_15ml",
|
||||
"object": "source",
|
||||
}
|
||||
for i, tk in enumerate(target_keys, start=1):
|
||||
info[tk] = {
|
||||
"slot": 2 + i, # 错开 source 使用的 slot
|
||||
"well": ["A1"],
|
||||
"labware": "nest_96_wellplate_2ml_deep",
|
||||
"object": "target",
|
||||
}
|
||||
info["tiprack_12"] = {
|
||||
"slot": 12,
|
||||
"well": [],
|
||||
"labware": "opentrons_96_tiprack_300ul",
|
||||
"object": "tiprack",
|
||||
}
|
||||
return info
|
||||
|
||||
|
||||
# ==================== 用例 ====================
|
||||
|
||||
|
||||
def test_per_plate_skipped_when_covered_by_merged():
|
||||
"""单 list-targets transfer 覆盖 4 个 target reagent_keys → per-plate 不再出现。"""
|
||||
targets = ["t_A", "t_B", "t_C", "t_D"]
|
||||
labware = _labware_with_targets(targets, source_keys=["src_1"])
|
||||
steps = [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_1",
|
||||
"targets": targets,
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [8.0] * 4,
|
||||
"dis_vols": [8.0] * 4,
|
||||
},
|
||||
"step_number": 1,
|
||||
}
|
||||
]
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=steps,
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
per_plate, merged = _set_liquid_nodes_split(g)
|
||||
|
||||
# merged 节点:1 个
|
||||
assert len(merged) == 1, f"应有 1 个 merged 节点;实际 {len(merged)}"
|
||||
|
||||
# per-plate 节点:仅 source 1 个(src_1);target 端被全部跳过
|
||||
per_plate_names = {n.get("description", "") for n in per_plate}
|
||||
per_plate_keys = {
|
||||
n.get("description", "").replace("Set liquid: ", "")
|
||||
for n in per_plate
|
||||
}
|
||||
assert "src_1" in per_plate_keys, "source 端 per-plate 必须保留"
|
||||
for tk in targets:
|
||||
assert tk not in per_plate_keys, (
|
||||
f"§14:target reagent_key '{tk}' 已被 merged 覆盖,不应再有 per-plate 节点;"
|
||||
f" 实际 per_plate_keys={per_plate_keys}"
|
||||
)
|
||||
|
||||
|
||||
def test_per_plate_kept_when_also_referenced_by_str_targets():
|
||||
"""R1 缓解:t_A 既被 list-targets 引用,又被 str-targets 引用 → per-plate 必须保留。"""
|
||||
targets_list = ["t_A", "t_B", "t_C"]
|
||||
labware = _labware_with_targets(targets_list, source_keys=["src_1"])
|
||||
steps = [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_1",
|
||||
"targets": targets_list,
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [5.0] * 3,
|
||||
"dis_vols": [5.0] * 3,
|
||||
},
|
||||
"step_number": 1,
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_1",
|
||||
"targets": "t_A",
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [10.0],
|
||||
"dis_vols": [10.0],
|
||||
},
|
||||
"step_number": 2,
|
||||
},
|
||||
]
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=steps,
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
per_plate, merged = _set_liquid_nodes_split(g)
|
||||
per_plate_keys = {
|
||||
n.get("description", "").replace("Set liquid: ", "")
|
||||
for n in per_plate
|
||||
}
|
||||
|
||||
assert "t_A" in per_plate_keys, (
|
||||
f"R1:t_A 被 str transfer #2 引用,必须保留 per-plate 节点;"
|
||||
f" 实际 per_plate_keys={per_plate_keys}"
|
||||
)
|
||||
assert "t_B" not in per_plate_keys, "t_B 仅出现在 list-targets,应跳过"
|
||||
assert "t_C" not in per_plate_keys, "t_C 仅出现在 list-targets,应跳过"
|
||||
|
||||
# merged 节点数:1(仅 list-targets transfer #1 生成)
|
||||
assert len(merged) == 1
|
||||
|
||||
|
||||
def test_str_targets_protocol_unaffected():
|
||||
"""单 slot 协议(全 str-targets)→ 每个 target reagent_key 仍有 per-plate(零回归)。"""
|
||||
labware = _labware_with_targets(["t_A", "t_B"], source_keys=["src_1"])
|
||||
steps = [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_1",
|
||||
"targets": "t_A",
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [10.0],
|
||||
"dis_vols": [10.0],
|
||||
},
|
||||
"step_number": 1,
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_1",
|
||||
"targets": "t_B",
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [20.0],
|
||||
"dis_vols": [20.0],
|
||||
},
|
||||
"step_number": 2,
|
||||
},
|
||||
]
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=steps,
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
per_plate, merged = _set_liquid_nodes_split(g)
|
||||
per_plate_keys = {
|
||||
n.get("description", "").replace("Set liquid: ", "")
|
||||
for n in per_plate
|
||||
}
|
||||
|
||||
assert merged == [], "全 str-targets 协议不应触发 merged 节点"
|
||||
assert {"src_1", "t_A", "t_B"}.issubset(per_plate_keys), (
|
||||
f"单 slot 协议每个 reagent_key(含 source/target)都应保留 per-plate;"
|
||||
f" 实际 {per_plate_keys}"
|
||||
)
|
||||
|
||||
|
||||
def test_51b9a5_style_node_count():
|
||||
"""大规模场景:N 个 list-targets transfers,每个长度 M(同 source 不同跨板)。
|
||||
|
||||
构造:2 个 source(src_A1、src_A2)+ 9 个 target plates × 2 个 well = 18 target reagent_keys。
|
||||
2 个 transfer:
|
||||
- transfer #1: targets = [t_A1_1, t_A1_2, ..., t_A1_9](同 source src_A1,跨 9 plate)
|
||||
- transfer #2: targets = [t_A2_1, t_A2_2, ..., t_A2_9](同 source src_A2,跨 9 plate)
|
||||
|
||||
期望 set_liquid_from_plate 总节点数 = 2 source per-plate + 2 merged + 0 target per-plate = 4。
|
||||
"""
|
||||
target_keys_a1 = [f"t_A1_{i}" for i in range(1, 10)]
|
||||
target_keys_a2 = [f"t_A2_{i}" for i in range(1, 10)]
|
||||
all_target_keys = target_keys_a1 + target_keys_a2
|
||||
|
||||
labware = _labware_with_targets(
|
||||
all_target_keys,
|
||||
source_keys=["src_A1", "src_A2"],
|
||||
)
|
||||
|
||||
steps = [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_A1",
|
||||
"targets": target_keys_a1,
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [8.3] * 9,
|
||||
"dis_vols": [8.3] * 9,
|
||||
},
|
||||
"step_number": 1,
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_A2",
|
||||
"targets": target_keys_a2,
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [8.3] * 9,
|
||||
"dis_vols": [8.3] * 9,
|
||||
},
|
||||
"step_number": 2,
|
||||
},
|
||||
]
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=steps,
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
per_plate, merged = _set_liquid_nodes_split(g)
|
||||
|
||||
assert len(merged) == 2, f"应有 2 个 merged 节点;实际 {len(merged)}"
|
||||
|
||||
per_plate_keys = {
|
||||
n.get("description", "").replace("Set liquid: ", "")
|
||||
for n in per_plate
|
||||
}
|
||||
|
||||
# source 端:2 个 per-plate
|
||||
assert "src_A1" in per_plate_keys and "src_A2" in per_plate_keys, (
|
||||
f"source 端必须有 src_A1 + src_A2 per-plate;实际 {per_plate_keys}"
|
||||
)
|
||||
|
||||
# target 端:18 个全部被跳过
|
||||
for tk in all_target_keys:
|
||||
assert tk not in per_plate_keys, (
|
||||
f"§14:target reagent_key '{tk}' 应被 merged 覆盖并跳过;"
|
||||
f" 实际 per_plate_keys 包含 {tk}"
|
||||
)
|
||||
|
||||
# 总节点数 == 2 + 2
|
||||
assert len(per_plate) + len(merged) == 4, (
|
||||
f"set_liquid_from_plate 总节点数应为 4 (2 source + 2 merged + 0 target per-plate);"
|
||||
f" 实际 per_plate={len(per_plate)} merged={len(merged)}"
|
||||
)
|
||||
|
||||
|
||||
def test_source_per_plate_always_kept():
|
||||
"""source reagent_keys 不出现在任何 targets 字段中 → per-plate 节点恒保留(与 §14 无关)。"""
|
||||
target_keys = ["t_A", "t_B", "t_C"]
|
||||
labware = _labware_with_targets(target_keys, source_keys=["src_X", "src_Y"])
|
||||
|
||||
steps = [
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_X",
|
||||
"targets": target_keys,
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [5.0] * 3,
|
||||
"dis_vols": [5.0] * 3,
|
||||
},
|
||||
"step_number": 1,
|
||||
},
|
||||
{
|
||||
"action": "transfer_liquid",
|
||||
"parameters": {
|
||||
"sources": "src_Y",
|
||||
"targets": "t_A",
|
||||
"tip_racks": "tiprack_12",
|
||||
"asp_vols": [10.0],
|
||||
"dis_vols": [10.0],
|
||||
},
|
||||
"step_number": 2,
|
||||
},
|
||||
]
|
||||
g = build_protocol_graph(
|
||||
labware_info=labware,
|
||||
protocol_steps=steps,
|
||||
workstation_name="PRCXI",
|
||||
)
|
||||
|
||||
per_plate, _ = _set_liquid_nodes_split(g)
|
||||
per_plate_keys = {
|
||||
n.get("description", "").replace("Set liquid: ", "")
|
||||
for n in per_plate
|
||||
}
|
||||
|
||||
assert "src_X" in per_plate_keys, "source src_X 必须有 per-plate(source 不会被 §14 跳过)"
|
||||
assert "src_Y" in per_plate_keys, "source src_Y 必须有 per-plate"
|
||||
534
tests/workflow/test_labware_mapping.py
Normal file
534
tests/workflow/test_labware_mapping.py
Normal file
@@ -0,0 +1,534 @@
|
||||
"""P6 / P6.1 / P6.1.1 `labware_mapping.py` 单元测试 —— 对应 06-labware-mapping-table.md §11.7.7 / §11.8.7。
|
||||
|
||||
这些用例只依赖 `unilabos.workflow.labware_mapping` 自身与 PyYAML,
|
||||
不需要 ROS2 / matplotlib / networkx 等环境,可直接 `pytest tests/workflow/test_labware_mapping.py`。
|
||||
|
||||
P6.1.1 schema(v1.9):
|
||||
- 顶层 key 两段:``kinds`` / ``target_devices``(**P6.1.1 起顶层 `slot_remap` 已不支持**,下沉到 ``target_devices.<device>`` 内)
|
||||
- ``target_devices.default`` 是固定段名,作为兜底物料集,第一版按 prcxi 拷贝填充,**不支持 `models` 子段**
|
||||
- ``target_devices.<device>.models.<model>`` 是可选的型号粒度覆盖(slot_remap / rules)
|
||||
- 旧 schema(顶层 ``vendors`` / ``slot_remap`` 或 rule 含 ``prcxi_class``)会触发 warning + fallback 到 builtin
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
from unilabos.workflow import labware_mapping as lm
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_lru_cache():
|
||||
"""每个用例后清缓存,避免 monkeypatch 跨用例污染。"""
|
||||
yield
|
||||
lm.reload_mapping()
|
||||
|
||||
|
||||
# ==================== slot_remap ====================
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw,object_type,want",
|
||||
[
|
||||
("4", "", "13"),
|
||||
("8", "", "14"),
|
||||
("12", "trash", "16"),
|
||||
("12", "source", "12"),
|
||||
("1", "", "1"),
|
||||
("", "", ""),
|
||||
(4, "", "13"), # 非字符串入参也应规整
|
||||
],
|
||||
)
|
||||
def test_remap_slot_basic(raw, object_type, want):
|
||||
assert lm.remap_slot(raw, object_type) == want
|
||||
|
||||
|
||||
def test_remap_slot_none_returns_empty():
|
||||
assert lm.remap_slot(None) == ""
|
||||
|
||||
|
||||
def test_remap_slot_passthrough_unknown():
|
||||
assert lm.remap_slot("99") == "99"
|
||||
|
||||
|
||||
# ==================== infer_kind ====================
|
||||
|
||||
|
||||
def test_infer_kind_trash_priority():
|
||||
"""`trash` 在 kinds 列表第 1 条 → 优先于含 'rack' 的字符串。"""
|
||||
assert lm.infer_kind("foo_trash_bar") == "trash"
|
||||
assert lm.infer_kind("opentrons_fixed_trash") == "trash"
|
||||
|
||||
|
||||
def test_infer_kind_tiprack_before_tuberack():
|
||||
"""`tiprack` 子串包含 'rack',但应被 tip_rack 规则先抓到(顺序敏感)。"""
|
||||
assert lm.infer_kind("opentrons_96_tiprack_300ul") == "tip_rack"
|
||||
assert lm.infer_kind("opentrons_96_tiprack_20ul") == "tip_rack"
|
||||
|
||||
|
||||
def test_infer_kind_tube_rack_variants():
|
||||
assert (
|
||||
lm.infer_kind("opentrons_24_tuberack_eppendorf_2ml_safelock_snapcap")
|
||||
== "tube_rack"
|
||||
)
|
||||
assert lm.infer_kind("opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical") == "tube_rack"
|
||||
|
||||
|
||||
def test_infer_kind_object_overrides_string():
|
||||
"""object 字段优先:即使字符串看起来像 plate,trash / tiprack 也能强制归类。"""
|
||||
assert lm.infer_kind("anything_at_all", "tiprack") == "tip_rack"
|
||||
assert lm.infer_kind("opentrons_96_wellplate", "trash") == "trash"
|
||||
|
||||
|
||||
def test_infer_kind_default_plate():
|
||||
assert lm.infer_kind("opentrons_96_wellplate_300ul_pcr") == "plate"
|
||||
assert lm.infer_kind("custom_384_wellplate_2200ul") == "plate"
|
||||
|
||||
|
||||
def test_infer_kind_rack_without_tip_is_tube_rack():
|
||||
"""复现历史 `_infer_reagent_kind` 中「含 rack 不含 tip → tube_rack」的语义。"""
|
||||
assert lm.infer_kind("nest_4x6_rack") == "tube_rack"
|
||||
|
||||
|
||||
def test_infer_kind_empty_hint_returns_plate():
|
||||
assert lm.infer_kind("") == "plate"
|
||||
assert lm.infer_kind(None) == "plate" # type: ignore[arg-type]
|
||||
|
||||
|
||||
# ==================== resolve_target_class(target_device="prcxi") ====================
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"vol,want",
|
||||
[
|
||||
(1, "PRCXI_10uL_Tips"),
|
||||
(9, "PRCXI_10uL_Tips"),
|
||||
(10, "PRCXI_10uL_Tips"), # 闭区间 ≤10
|
||||
(11, "PRCXI_300ul_Tips"),
|
||||
(200, "PRCXI_300ul_Tips"),
|
||||
(299.9, "PRCXI_300ul_Tips"),
|
||||
(300, "PRCXI_1000uL_Tips"), # 300 上一档(与 <300 半开等价)
|
||||
(500, "PRCXI_1000uL_Tips"),
|
||||
(1000, "PRCXI_1000uL_Tips"),
|
||||
],
|
||||
)
|
||||
def test_resolve_tip_volume_buckets(vol, want):
|
||||
assert lm.resolve_target_class("prcxi", "tip_rack", 96, vol) == want
|
||||
|
||||
|
||||
def test_resolve_tube_rack_holes():
|
||||
assert lm.resolve_target_class("prcxi", "tube_rack", 24, None) == "PRCXI_EP_Adapter"
|
||||
assert lm.resolve_target_class("prcxi", "tube_rack", 10, None) == "PRCXI_EP_Adapter"
|
||||
|
||||
|
||||
def test_resolve_plate_holes():
|
||||
assert lm.resolve_target_class("prcxi", "plate", 96, None) == "PRCXI_BioER_96_wellplate"
|
||||
assert (
|
||||
lm.resolve_target_class("prcxi", "plate", 384, None) == "PRCXI_BioER_384_wellplate"
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_plate_unknown_holes_returns_none():
|
||||
"""48 孔板未在 YAML 列出 → None;交给 PRCXI 模板打分匹配 fallback。"""
|
||||
assert lm.resolve_target_class("prcxi", "plate", 48, 2200) is None
|
||||
|
||||
|
||||
def test_resolve_trash_any():
|
||||
assert lm.resolve_target_class("prcxi", "trash", None, None) == "PRCXI_trash"
|
||||
# trash 规则未约束 hole_count / volume,所以任意值都命中
|
||||
assert lm.resolve_target_class("prcxi", "trash", 0, 0) == "PRCXI_trash"
|
||||
|
||||
|
||||
# ==================== YAML 缺失 / 热加载 ====================
|
||||
|
||||
|
||||
def test_missing_yaml_uses_builtin(monkeypatch, tmp_path):
|
||||
"""YAML 文件不存在时,应自动落到 `_BUILTIN_DEFAULT`,且打 warning。"""
|
||||
bogus = tmp_path / "no_such_labware_mapping.yaml"
|
||||
monkeypatch.setattr(lm, "_DEFAULT_PATH", bogus)
|
||||
lm._load_mapping.cache_clear()
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
assert lm.remap_slot("4") == "13"
|
||||
assert (
|
||||
lm.resolve_target_class("prcxi", "plate", 96, None)
|
||||
== "PRCXI_BioER_96_wellplate"
|
||||
)
|
||||
assert any("labware_mapping.yaml 未找到" in str(w.message) for w in caught)
|
||||
|
||||
|
||||
def test_invalid_yaml_uses_builtin(monkeypatch, tmp_path):
|
||||
"""YAML 解析失败也应回退到 builtin,且打 warning。"""
|
||||
bad = tmp_path / "labware_mapping.yaml"
|
||||
bad.write_text("this is :: not valid: yaml: [unclosed", encoding="utf-8")
|
||||
monkeypatch.setattr(lm, "_DEFAULT_PATH", bad)
|
||||
lm._load_mapping.cache_clear()
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
assert lm.remap_slot("4") == "13"
|
||||
assert any(
|
||||
"labware_mapping.yaml 解析失败" in str(w.message)
|
||||
or "labware_mapping.yaml 根不是 dict" in str(w.message)
|
||||
for w in caught
|
||||
)
|
||||
|
||||
|
||||
def test_yaml_reload_after_edit(monkeypatch, tmp_path):
|
||||
"""临时 YAML 覆盖 + reload_mapping → 新规则生效,且原规则失效(P6.1.1 schema)。"""
|
||||
tmp_yaml = tmp_path / "labware_mapping.yaml"
|
||||
tmp_yaml.write_text(
|
||||
'kinds:\n'
|
||||
" - { pattern: 'trash', kind: trash }\n"
|
||||
" - { pattern: '.*', kind: plate }\n"
|
||||
'target_devices:\n'
|
||||
' default:\n'
|
||||
' slot_remap:\n'
|
||||
' default: {"4": "99"}\n'
|
||||
' by_object: {}\n'
|
||||
' rules:\n'
|
||||
" - { kind: plate, hole_count: 96, class_name: PRCXI_FooPlate }\n"
|
||||
' prcxi:\n'
|
||||
' slot_remap:\n'
|
||||
' default: {"4": "99"}\n'
|
||||
' by_object: {}\n'
|
||||
' rules:\n'
|
||||
" - { kind: plate, hole_count: 96, class_name: PRCXI_FooPlate }\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(lm, "_DEFAULT_PATH", tmp_yaml)
|
||||
lm.reload_mapping()
|
||||
assert lm.remap_slot("4") == "99"
|
||||
assert lm.resolve_target_class("prcxi", "plate", 96, None) == "PRCXI_FooPlate"
|
||||
# 新表里只有 96,没有 384 → None
|
||||
assert lm.resolve_target_class("prcxi", "plate", 384, None) is None
|
||||
# tube_rack / tip_rack 在新表里没规则 → None
|
||||
assert lm.resolve_target_class("prcxi", "tip_rack", 96, 200) is None
|
||||
|
||||
|
||||
def test_missing_section_uses_builtin(monkeypatch, tmp_path):
|
||||
"""YAML 缺 `kinds` 段 → 该段使用 builtin,其它段保留用户值(P6.1.1 schema)。"""
|
||||
partial = tmp_path / "labware_mapping.yaml"
|
||||
partial.write_text(
|
||||
'target_devices:\n'
|
||||
' default:\n'
|
||||
' slot_remap:\n'
|
||||
' default: {"4": "88"}\n'
|
||||
' by_object: {}\n'
|
||||
' rules: []\n'
|
||||
' prcxi:\n'
|
||||
' slot_remap:\n'
|
||||
' default: {"4": "88"}\n'
|
||||
' by_object: {}\n'
|
||||
' rules: []\n', # 故意没有 kinds 段
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(lm, "_DEFAULT_PATH", partial)
|
||||
lm._load_mapping.cache_clear()
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
# slot_remap 用 YAML 中的覆盖值
|
||||
assert lm.remap_slot("4") == "88"
|
||||
# kinds 段缺失 → 使用 builtin 的 tiprack 规则
|
||||
assert lm.infer_kind("opentrons_96_tiprack_300ul") == "tip_rack"
|
||||
assert any("缺少 `kinds` 段" in str(w.message) for w in caught)
|
||||
|
||||
|
||||
# ==================== P6.1 新增用例 ====================
|
||||
|
||||
|
||||
def test_resolve_target_class_prcxi_tip_buckets():
|
||||
"""PRCXI tip 量程档:≤10 / <300 / 否则 1000(与历史 _tip_prcxi_class_for_max_ul 等价)。"""
|
||||
assert lm.resolve_target_class("prcxi", "tip_rack", 96, 10) == "PRCXI_10uL_Tips"
|
||||
assert lm.resolve_target_class("prcxi", "tip_rack", 96, 200) == "PRCXI_300ul_Tips"
|
||||
assert lm.resolve_target_class("prcxi", "tip_rack", 96, 1000) == "PRCXI_1000uL_Tips"
|
||||
|
||||
|
||||
def test_resolve_target_class_unknown_device_falls_back_to_default_section():
|
||||
"""未声明的 target_device 自动回退到固定段 target_devices.default,打 warning。
|
||||
第一版 default 段内容按 prcxi 拷贝 → 断言:caller 传 'tecan' 时,结果应等于查 default 段。"""
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
# tecan / beckman / 任意未声明名字 → 全部回退到固定段 "default"
|
||||
assert (
|
||||
lm.resolve_target_class("tecan", "tip_rack", 96, 200)
|
||||
== lm.resolve_target_class("default", "tip_rack", 96, 200)
|
||||
== "PRCXI_300ul_Tips" # 第一版 default 段按 prcxi 填,所以值仍是 PRCXI_*
|
||||
)
|
||||
assert (
|
||||
lm.resolve_target_class("unknown_xxx", "plate", 96, None)
|
||||
== lm.resolve_target_class("default", "plate", 96, None)
|
||||
)
|
||||
# 至少打 1 次 warning,提示「未声明、已回退到 default 段」
|
||||
assert any(
|
||||
("未在 labware_mapping.yaml" in str(w.message))
|
||||
or ("target_devices.default" in str(w.message))
|
||||
for w in caught
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_target_class_per_device_tip_buckets(tmp_path, monkeypatch):
|
||||
"""**P6.1 核心断言**:不同 target_device 在同一体积下命中不同 tip 量程档(P6.1.1 schema)。"""
|
||||
yaml_path = tmp_path / "labware_mapping.yaml"
|
||||
yaml_path.write_text(
|
||||
'kinds: [{pattern: ".*", kind: plate}]\n'
|
||||
'target_devices:\n'
|
||||
' default:\n'
|
||||
' slot_remap: {default: {}, by_object: {}}\n'
|
||||
' rules:\n'
|
||||
' - {kind: tip_rack, hole_count: 96, volume_max: 10, class_name: PRCXI_10uL_Tips}\n'
|
||||
' - {kind: tip_rack, hole_count: 96, volume_max: 299.9, class_name: PRCXI_300ul_Tips}\n'
|
||||
' - {kind: tip_rack, hole_count: 96, class_name: PRCXI_1000uL_Tips}\n'
|
||||
' prcxi:\n'
|
||||
' slot_remap: {default: {}, by_object: {}}\n'
|
||||
' rules:\n'
|
||||
' - {kind: tip_rack, hole_count: 96, volume_max: 10, class_name: PRCXI_10uL_Tips}\n'
|
||||
' - {kind: tip_rack, hole_count: 96, volume_max: 299.9, class_name: PRCXI_300ul_Tips}\n'
|
||||
' - {kind: tip_rack, hole_count: 96, class_name: PRCXI_1000uL_Tips}\n'
|
||||
' beckman:\n'
|
||||
' slot_remap: {default: {}, by_object: {}}\n'
|
||||
' rules:\n'
|
||||
' - {kind: tip_rack, hole_count: 96, volume_max: 20, class_name: Beckman_20uL_Tips}\n'
|
||||
' - {kind: tip_rack, hole_count: 96, volume_max: 199.9, class_name: Beckman_200uL_Tips}\n'
|
||||
' - {kind: tip_rack, hole_count: 96, class_name: Beckman_1000uL_Tips}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(lm, "_DEFAULT_PATH", yaml_path)
|
||||
lm.reload_mapping()
|
||||
|
||||
# 同样的体积 200:prcxi 走 300 档、beckman 已超出 200 档 → 1000 档
|
||||
assert lm.resolve_target_class("prcxi", "tip_rack", 96, 200) == "PRCXI_300ul_Tips"
|
||||
assert lm.resolve_target_class("beckman", "tip_rack", 96, 200) == "Beckman_1000uL_Tips"
|
||||
# 同样的体积 15:prcxi 已超出 10 档 → 300 档;beckman 仍在 20 档
|
||||
assert lm.resolve_target_class("prcxi", "tip_rack", 96, 15) == "PRCXI_300ul_Tips"
|
||||
assert lm.resolve_target_class("beckman", "tip_rack", 96, 15) == "Beckman_20uL_Tips"
|
||||
|
||||
|
||||
def test_default_section_independent_from_prcxi(tmp_path, monkeypatch):
|
||||
"""default 与 prcxi 是两段独立物料集:改 default 不影响 prcxi、改 prcxi 不影响 default。
|
||||
|
||||
断言:把 default 段改成 Generic_Plate96,prcxi 段保持 PRCXI_Plate96 时,
|
||||
caller 传未声明的名字回退到 default 拿 Generic_Plate96,传 prcxi 仍拿 PRCXI_Plate96。
|
||||
"""
|
||||
yaml_path = tmp_path / "labware_mapping.yaml"
|
||||
yaml_path.write_text(
|
||||
'kinds: [{pattern: ".*", kind: plate}]\n'
|
||||
'target_devices:\n'
|
||||
' default:\n' # ← 独立改 default 段
|
||||
' slot_remap: {default: {}, by_object: {}}\n'
|
||||
' rules:\n'
|
||||
' - {kind: plate, hole_count: 96, class_name: Generic_Plate96}\n'
|
||||
' prcxi:\n' # ← prcxi 段保持 PRCXI_*
|
||||
' slot_remap: {default: {}, by_object: {}}\n'
|
||||
' rules:\n'
|
||||
' - {kind: plate, hole_count: 96, class_name: PRCXI_Plate96}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(lm, "_DEFAULT_PATH", yaml_path)
|
||||
lm.reload_mapping()
|
||||
|
||||
# caller 传未声明的 tecan → 走 default 段 → Generic_*
|
||||
assert lm.resolve_target_class("tecan", "plate", 96, None) == "Generic_Plate96"
|
||||
# caller 显式传 prcxi → 走 prcxi 段 → PRCXI_*(**不**受 default 影响)
|
||||
assert lm.resolve_target_class("prcxi", "plate", 96, None) == "PRCXI_Plate96"
|
||||
# 显式传 "default" 也合法(caller 可主动选择走 default 段)
|
||||
assert lm.resolve_target_class("default", "plate", 96, None) == "Generic_Plate96"
|
||||
|
||||
|
||||
def test_legacy_yaml_schema_rejected_with_warning(tmp_path, monkeypatch):
|
||||
"""旧 schema(vendors / prcxi_class)应被拒绝 + warning + 整段 fallback 到 builtin(P6.1.1 schema)。"""
|
||||
legacy = tmp_path / "labware_mapping.yaml"
|
||||
legacy.write_text(
|
||||
'kinds: [{pattern: ".*", kind: plate}]\n'
|
||||
'vendors:\n' # ← 旧顶层 key
|
||||
' opentrons:\n'
|
||||
' rules:\n'
|
||||
" - {kind: plate, hole_count: 96, prcxi_class: PRCXI_FooPlate}\n", # ← 旧字段
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(lm, "_DEFAULT_PATH", legacy)
|
||||
lm._load_mapping.cache_clear()
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
# 整段走 builtin → 96 板还是 PRCXI_BioER_96_wellplate(**不是**用户旧 YAML 中的 PRCXI_FooPlate)
|
||||
assert lm.resolve_target_class("prcxi", "plate", 96, None) == "PRCXI_BioER_96_wellplate"
|
||||
assert any(
|
||||
("旧 schema" in str(w.message))
|
||||
or ("vendors" in str(w.message))
|
||||
or ("prcxi_class" in str(w.message))
|
||||
for w in caught
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_target_class_unknown_kind_returns_none():
|
||||
"""target_device 存在、kind 不存在 → None。"""
|
||||
assert lm.resolve_target_class("prcxi", "reservoir", 12, None) is None
|
||||
|
||||
|
||||
# ==================== P6.1.1 新增用例(slot_remap 按 device + model 分叉) ====================
|
||||
|
||||
|
||||
def test_remap_slot_model_level_overrides_device_level(tmp_path, monkeypatch):
|
||||
"""型号级 slot_remap 优先级 > 厂商级。"""
|
||||
yaml_path = tmp_path / "labware_mapping.yaml"
|
||||
yaml_path.write_text(
|
||||
'kinds: [{pattern: ".*", kind: plate}]\n'
|
||||
'target_devices:\n'
|
||||
' default:\n'
|
||||
' slot_remap: {default: {"4": "13"}, by_object: {}}\n'
|
||||
' rules: []\n'
|
||||
' prcxi:\n'
|
||||
' slot_remap: {default: {"4": "13"}, by_object: {trash: {"12": "16"}}}\n'
|
||||
' rules: []\n'
|
||||
' models:\n'
|
||||
' "4040":\n'
|
||||
' slot_remap: {default: {"4": "16"}, by_object: {}}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(lm, "_DEFAULT_PATH", yaml_path)
|
||||
lm.reload_mapping()
|
||||
# device 级(不传 model)→ "13"
|
||||
assert lm.remap_slot("4", target_device="prcxi") == "13"
|
||||
# model "4040" 覆盖 → "16"
|
||||
assert lm.remap_slot("4", target_device="prcxi", target_model="4040") == "16"
|
||||
# model "9320" 未声明 → 静默 fallback 到 device 级 → "13"
|
||||
assert lm.remap_slot("4", target_device="prcxi", target_model="9320") == "13"
|
||||
|
||||
|
||||
def test_remap_slot_model_inherits_device_when_field_missing(tmp_path, monkeypatch):
|
||||
"""model 子段声明但 slot_remap 字段缺失 → 静默继承厂商级;rules 同理。"""
|
||||
yaml_path = tmp_path / "labware_mapping.yaml"
|
||||
yaml_path.write_text(
|
||||
'kinds: [{pattern: ".*", kind: plate}]\n'
|
||||
'target_devices:\n'
|
||||
' default:\n'
|
||||
' slot_remap: {default: {}, by_object: {}}\n'
|
||||
' rules: []\n'
|
||||
' prcxi:\n'
|
||||
' slot_remap: {default: {"4": "13", "8": "14"}, by_object: {}}\n'
|
||||
' rules: [{kind: plate, hole_count: 96, class_name: PRCXI_PlateA}]\n'
|
||||
' models:\n'
|
||||
' "9320":\n'
|
||||
' rules: [{kind: plate, hole_count: 96, class_name: PRCXI_PlateB}]\n', # 仅覆盖 rules,未声明 slot_remap
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(lm, "_DEFAULT_PATH", yaml_path)
|
||||
lm.reload_mapping()
|
||||
# model 9320 的 slot_remap 缺字段 → 继承 prcxi.slot_remap → "4" → "13"
|
||||
assert lm.remap_slot("4", target_device="prcxi", target_model="9320") == "13"
|
||||
# model 9320 的 rules 覆盖 → PRCXI_PlateB
|
||||
assert (
|
||||
lm.resolve_target_class("prcxi", "plate", 96, None, target_model="9320")
|
||||
== "PRCXI_PlateB"
|
||||
)
|
||||
# 不传 model → 用厂商级 rules → PRCXI_PlateA
|
||||
assert lm.resolve_target_class("prcxi", "plate", 96, None) == "PRCXI_PlateA"
|
||||
|
||||
|
||||
def test_legacy_top_level_slot_remap_rejected(tmp_path, monkeypatch):
|
||||
"""P6.1.1:顶层 slot_remap 段被视为旧 schema → warning + 整段 fallback 到 builtin。"""
|
||||
legacy = tmp_path / "labware_mapping.yaml"
|
||||
legacy.write_text(
|
||||
'slot_remap:\n' # ← P6.1.1 已不支持的顶层段
|
||||
' default: {"4": "99"}\n'
|
||||
' by_object: {}\n'
|
||||
'kinds: [{pattern: ".*", kind: plate}]\n'
|
||||
'target_devices:\n'
|
||||
' default:\n'
|
||||
' slot_remap: {default: {"4": "13"}, by_object: {}}\n'
|
||||
' rules: []\n'
|
||||
' prcxi:\n'
|
||||
' slot_remap: {default: {"4": "13"}, by_object: {}}\n'
|
||||
' rules: []\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(lm, "_DEFAULT_PATH", legacy)
|
||||
lm._load_mapping.cache_clear()
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
# 整段走 builtin → "4" 仍然 → "13"(builtin 值),**不是** YAML 顶层的 "99"
|
||||
assert lm.remap_slot("4", target_device="prcxi") == "13"
|
||||
assert any(
|
||||
("顶层" in str(w.message) and "slot_remap" in str(w.message))
|
||||
or ("旧 schema" in str(w.message))
|
||||
for w in caught
|
||||
)
|
||||
|
||||
|
||||
def test_remap_slot_unknown_device_falls_back_with_warning(tmp_path, monkeypatch):
|
||||
"""未声明的 target_device → fallback 到 default.slot_remap + warning(与 resolve_target_class 同语义)。"""
|
||||
yaml_path = tmp_path / "labware_mapping.yaml"
|
||||
yaml_path.write_text(
|
||||
'kinds: [{pattern: ".*", kind: plate}]\n'
|
||||
'target_devices:\n'
|
||||
' default:\n'
|
||||
' slot_remap: {default: {"4": "13"}, by_object: {}}\n'
|
||||
' rules: []\n'
|
||||
' prcxi:\n'
|
||||
' slot_remap: {default: {"4": "13"}, by_object: {}}\n'
|
||||
' rules: []\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(lm, "_DEFAULT_PATH", yaml_path)
|
||||
lm.reload_mapping()
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
assert lm.remap_slot("4", target_device="tecan") == "13" # fallback 到 default
|
||||
assert any(
|
||||
("tecan" in str(w.message)) or ("target_devices.default" in str(w.message))
|
||||
for w in caught
|
||||
)
|
||||
|
||||
|
||||
def test_remap_slot_model_only_no_device_passthrough(tmp_path, monkeypatch):
|
||||
"""caller 传 target_model 但 target_device 段不存在 → 直接走 default.slot_remap(model 名忽略)。"""
|
||||
yaml_path = tmp_path / "labware_mapping.yaml"
|
||||
yaml_path.write_text(
|
||||
'kinds: [{pattern: ".*", kind: plate}]\n'
|
||||
'target_devices:\n'
|
||||
' default:\n'
|
||||
' slot_remap: {default: {"4": "13"}, by_object: {}}\n'
|
||||
' rules: []\n', # 没有 prcxi 段
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(lm, "_DEFAULT_PATH", yaml_path)
|
||||
lm.reload_mapping()
|
||||
with warnings.catch_warnings(record=True):
|
||||
warnings.simplefilter("always")
|
||||
# target_device "prcxi" 不存在、target_model 即使传也忽略 → 走 default
|
||||
assert lm.remap_slot("4", target_device="prcxi", target_model="9320") == "13"
|
||||
|
||||
|
||||
def test_default_section_models_subsection_warns(tmp_path, monkeypatch):
|
||||
"""target_devices.default.models 不被支持 → warning,但 default.slot_remap 仍生效。"""
|
||||
yaml_path = tmp_path / "labware_mapping.yaml"
|
||||
yaml_path.write_text(
|
||||
'kinds: [{pattern: ".*", kind: plate}]\n'
|
||||
'target_devices:\n'
|
||||
' default:\n'
|
||||
' slot_remap: {default: {"4": "13"}, by_object: {}}\n'
|
||||
' rules: []\n'
|
||||
' models:\n' # ← default 段不支持 models
|
||||
' "ghost":\n'
|
||||
' slot_remap: {default: {"4": "99"}, by_object: {}}\n'
|
||||
' prcxi:\n'
|
||||
' slot_remap: {default: {"4": "13"}, by_object: {}}\n'
|
||||
' rules: []\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr(lm, "_DEFAULT_PATH", yaml_path)
|
||||
lm._load_mapping.cache_clear()
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
# default 段的 models 被忽略 → 走 default.slot_remap → "13"(不是 "99")
|
||||
assert lm.remap_slot("4", target_device="tecan", target_model="ghost") == "13"
|
||||
assert any(
|
||||
("default" in str(w.message) and "models" in str(w.message))
|
||||
for w in caught
|
||||
)
|
||||
178
tests/workflow/test_wf_utils_workflow_name.py
Normal file
178
tests/workflow/test_wf_utils_workflow_name.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""``unilabos.workflow.wf_utils.upload_workflow`` 工作流名称 fallback 链单元测试。
|
||||
|
||||
对应需求:上传工作流时,**优先取 metadata.workflow_name**;缺失时再回退到顶层
|
||||
``workflow_name``(旧 node-link 形态遗留字段);最后才回退到文件名(去 ``.json`` 后缀)。
|
||||
CLI 显式 ``-n/--workflow_name`` 永远最优先。
|
||||
|
||||
本测试只校验「**名称 fallback 链 + tags fallback 链**」的纯逻辑路径,
|
||||
不实际访问 HTTP / 后端;通过 monkeypatch 把 ``http_client.workflow_import``
|
||||
桩成可观察的捕获函数。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# 让 import 走 Uni-Lab-OS 包根
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SRC = ROOT / "unilabos"
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stub_upload(monkeypatch, tmp_path):
|
||||
"""Monkeypatch ``http_client.workflow_import`` + ``_convert_to_node_link``,
|
||||
返回 (helper, captured) 二元组:
|
||||
|
||||
- ``helper(workflow_data, **upload_kwargs)`` 写入 tmp_path/wf.json
|
||||
并调用 ``upload_workflow``;
|
||||
- ``captured`` 是 dict,记录 ``workflow_import`` 实际收到的 kwargs,
|
||||
以及 ``_convert_to_node_link`` 是否被调过。
|
||||
|
||||
本测试不依赖真实 ``unilabos.app.web``(其级联依赖含 ``fastapi`` 等重型
|
||||
package,本地 dev venv 不必装)。通过在 sys.modules 注入空壳 module 拦截
|
||||
delayed import。
|
||||
"""
|
||||
import types
|
||||
|
||||
captured: Dict[str, Any] = {"workflow_import_kwargs": None, "converted": False}
|
||||
|
||||
def fake_workflow_import(**kwargs): # noqa: ANN003
|
||||
captured["workflow_import_kwargs"] = kwargs
|
||||
return {"code": 0, "data": {"uuid": "fake-uuid", "name": kwargs.get("name")}}
|
||||
|
||||
# 关键:在 wf_utils 触发 `from unilabos.app.web import http_client` 之前
|
||||
# 用空壳 module 占位(避免触发真实 web 包的 fastapi 依赖链)。
|
||||
fake_http_client = types.ModuleType("unilabos.app.web.http_client")
|
||||
fake_http_client.workflow_import = fake_workflow_import # type: ignore[attr-defined]
|
||||
fake_web_pkg = types.ModuleType("unilabos.app.web")
|
||||
fake_web_pkg.http_client = fake_http_client # type: ignore[attr-defined]
|
||||
monkeypatch.setitem(sys.modules, "unilabos.app.web", fake_web_pkg)
|
||||
monkeypatch.setitem(sys.modules, "unilabos.app.web.http_client", fake_http_client)
|
||||
|
||||
from unilabos.workflow import wf_utils
|
||||
|
||||
# _convert_to_node_link 走真实路径会拉重型依赖,这里桩为 node-link 直返回
|
||||
def fake_convert_to_node_link(workflow_file, workflow_data, *, target_device="prcxi", target_model=None):
|
||||
captured["converted"] = True
|
||||
# 返回最小合法 node-link 形态(不带 metadata,模拟当前行为)
|
||||
return {"nodes": [], "edges": [], "workflow_uuid": ""}
|
||||
|
||||
monkeypatch.setattr(wf_utils, "_convert_to_node_link", fake_convert_to_node_link)
|
||||
|
||||
def helper(workflow_data: Dict[str, Any], **upload_kwargs: Any) -> Dict[str, Any]:
|
||||
wf_path = tmp_path / "transfer_actions_sample.json"
|
||||
wf_path.write_text(json.dumps(workflow_data, ensure_ascii=False), encoding="utf-8")
|
||||
return wf_utils.upload_workflow(str(wf_path), **upload_kwargs)
|
||||
|
||||
return helper, captured
|
||||
|
||||
|
||||
# ==================== workflow_name fallback 链 ====================
|
||||
|
||||
|
||||
def test_metadata_workflow_name_wins_over_filename(stub_upload):
|
||||
"""P5 主路径:transfer_actions JSON 含 metadata.workflow_name → 优先于文件名。"""
|
||||
helper, captured = stub_upload
|
||||
data = {
|
||||
"metadata": {"workflow_name": "PCR Prep with Categories", "tags": []},
|
||||
"workflow": [],
|
||||
"reagent": {},
|
||||
}
|
||||
helper(data)
|
||||
kwargs = captured["workflow_import_kwargs"]
|
||||
assert kwargs is not None and captured["converted"] is True
|
||||
assert kwargs["name"] == "PCR Prep with Categories"
|
||||
assert kwargs["workflow_name"] == "PCR Prep with Categories"
|
||||
|
||||
|
||||
def test_cli_workflow_name_overrides_metadata(stub_upload):
|
||||
"""CLI 显式 -n/--workflow_name 永远最优先。"""
|
||||
helper, captured = stub_upload
|
||||
data = {
|
||||
"metadata": {"workflow_name": "Metadata Wins By Default"},
|
||||
"workflow": [],
|
||||
"reagent": {},
|
||||
}
|
||||
helper(data, workflow_name="CLI Override Name")
|
||||
kwargs = captured["workflow_import_kwargs"]
|
||||
assert kwargs["name"] == "CLI Override Name"
|
||||
assert kwargs["workflow_name"] == "CLI Override Name"
|
||||
|
||||
|
||||
def test_filename_used_when_no_metadata_and_no_legacy(stub_upload):
|
||||
"""P5 之前的旧文件、且无顶层 workflow_name → 回退到去 .json 后缀的文件名。"""
|
||||
helper, captured = stub_upload
|
||||
data = {"workflow": [], "reagent": {}} # 既无 metadata,也无 workflow_name
|
||||
helper(data)
|
||||
kwargs = captured["workflow_import_kwargs"]
|
||||
# 文件名由 fixture 固定为 transfer_actions_sample.json
|
||||
assert kwargs["name"] == "transfer_actions_sample"
|
||||
assert kwargs["workflow_name"] == "transfer_actions_sample"
|
||||
|
||||
|
||||
def test_metadata_empty_string_falls_back_to_filename(stub_upload):
|
||||
"""metadata.workflow_name 为空字符串(而非缺失)也应回退到文件名。"""
|
||||
helper, captured = stub_upload
|
||||
data = {
|
||||
"metadata": {"workflow_name": " "}, # whitespace-only
|
||||
"workflow": [],
|
||||
"reagent": {},
|
||||
}
|
||||
helper(data)
|
||||
kwargs = captured["workflow_import_kwargs"]
|
||||
assert kwargs["name"] == "transfer_actions_sample"
|
||||
|
||||
|
||||
def test_legacy_top_level_workflow_name_used_when_metadata_missing(stub_upload, monkeypatch):
|
||||
"""旧 node-link 文件(已是 nodes/edges 形态)顶层 workflow_name → 应被使用。
|
||||
|
||||
覆盖路径:``_is_node_link_format`` 直接命中 → 不走转换 → workflow_data 保留顶层
|
||||
workflow_name;``orig_metadata`` 为空时 fallback 到该字段。
|
||||
"""
|
||||
helper, captured = stub_upload
|
||||
data = {
|
||||
"nodes": [],
|
||||
"edges": [],
|
||||
"workflow_name": "Legacy Top Name",
|
||||
}
|
||||
helper(data)
|
||||
kwargs = captured["workflow_import_kwargs"]
|
||||
assert captured["converted"] is False, "node-link 输入不应触发转换"
|
||||
assert kwargs["name"] == "Legacy Top Name"
|
||||
assert kwargs["workflow_name"] == "Legacy Top Name"
|
||||
|
||||
|
||||
# ==================== tags fallback 链 ====================
|
||||
|
||||
|
||||
def test_metadata_tags_used_when_cli_tags_missing(stub_upload):
|
||||
"""P5 主路径:metadata.tags 在 CLI 未传 tags 时被使用。"""
|
||||
helper, captured = stub_upload
|
||||
data = {
|
||||
"metadata": {"workflow_name": "X", "tags": ["Opentrons", "PCR"]},
|
||||
"workflow": [],
|
||||
"reagent": {},
|
||||
}
|
||||
helper(data)
|
||||
kwargs = captured["workflow_import_kwargs"]
|
||||
assert kwargs["tags"] == ["Opentrons", "PCR"]
|
||||
|
||||
|
||||
def test_cli_tags_override_metadata_tags(stub_upload):
|
||||
"""CLI 显式 --tags 优先于 metadata.tags。"""
|
||||
helper, captured = stub_upload
|
||||
data = {
|
||||
"metadata": {"workflow_name": "X", "tags": ["Opentrons", "PCR"]},
|
||||
"workflow": [],
|
||||
"reagent": {},
|
||||
}
|
||||
helper(data, tags=["CLI", "Wins"])
|
||||
kwargs = captured["workflow_import_kwargs"]
|
||||
assert kwargs["tags"] == ["CLI", "Wins"]
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.11.2"
|
||||
__version__ = "0.11.1"
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
"""Entry point for `python -m unilabos`."""
|
||||
|
||||
from unilabos.app.main import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -336,6 +336,27 @@ def parse_args():
|
||||
default="",
|
||||
help="Workflow description, used when publishing the workflow",
|
||||
)
|
||||
workflow_parser.add_argument(
|
||||
"--target_device",
|
||||
type=str,
|
||||
default="prcxi",
|
||||
help=(
|
||||
"Target instrument name at vendor granularity (e.g. 'prcxi', 'beckman', 'tecan'). "
|
||||
"Decides which target_devices.<name>.rules section in labware_mapping.yaml is used. "
|
||||
"Unknown names fall back to target_devices.default. Default: 'prcxi'."
|
||||
),
|
||||
)
|
||||
workflow_parser.add_argument(
|
||||
"--target_model",
|
||||
type=str,
|
||||
default=None,
|
||||
help=(
|
||||
"Optional target instrument model name within the same vendor (e.g. '9320', '4040'). "
|
||||
"Used to look up target_devices.<target_device>.models.<target_model>.slot_remap / "
|
||||
".rules for model-specific deck layout or rule overrides. Falls back to the vendor-level "
|
||||
"configuration when omitted or the model is not declared. Default: None."
|
||||
),
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -630,8 +651,6 @@ def main():
|
||||
continue
|
||||
|
||||
# 如果从远端获取了物料信息,则与本地物料进行同步
|
||||
# 仅在本地文件模式下有意义:本地文件只含设备结构,远端有已保存的物料,需要 merge
|
||||
# 远端模式下 resource_tree_set 与 request_startup_json 来自同一份数据,merge 为空操作
|
||||
if file_path is not None and request_startup_json and "nodes" in request_startup_json:
|
||||
print_status("开始同步远端物料到本地...", "info")
|
||||
remote_tree_set = ResourceTreeSet.from_raw_dict_list(request_startup_json["nodes"])
|
||||
|
||||
@@ -59,7 +59,6 @@ class JobAddReq(BaseModel):
|
||||
task_id: str = Field(examples=["task_id"], description="task uuid (auto-generated if empty)", default="")
|
||||
job_id: str = Field(examples=["job_id"], description="goal uuid (auto-generated if empty)", default="")
|
||||
node_id: str = Field(examples=["node_id"], description="node uuid", default="")
|
||||
notebook_id: str = Field(examples=["notebook_id"], description="notebook uuid", default="")
|
||||
server_info: dict = Field(
|
||||
examples=[{"send_timestamp": 1717000000.0}],
|
||||
description="server info (auto-generated if empty)",
|
||||
|
||||
@@ -10,170 +10,29 @@ import shutil
|
||||
import sys
|
||||
|
||||
|
||||
_PATCH_MARKER = "# UniLabOS DLL Patch"
|
||||
_PATCH_END_MARKER = "# End UniLabOS DLL Patch"
|
||||
|
||||
# 75 = EX_TEMPFAIL: 临时失败、重试即可,避免与业务退出码冲突
|
||||
_RESTART_EXIT_CODE = 75
|
||||
|
||||
|
||||
def _build_dll_patch(lib_bin: str, preload_pyd: str = "") -> str:
|
||||
"""生成一段加在目标文件顶部的 DLL 加载补丁源码。
|
||||
|
||||
- 始终把 ``lib_bin`` 加入 DLL 搜索路径,并把 handle 挂在模块属性上,
|
||||
防止 GC 清掉搜索路径(``os.add_dll_directory`` 的句柄被回收时
|
||||
目录会被移除)。
|
||||
- 可选地用 ``ctypes.CDLL`` 预加载一个 .pyd,把它的依赖 DLL 提前装入
|
||||
进程内存,作为 ``rclpy._rclpy_pybind11`` 这类首次加载点的兜底。
|
||||
"""
|
||||
# 用 repr() 序列化路径:Python 解析 repr 的结果会还原成原始字符串,
|
||||
# 不需要也不能再叠加 raw-string 前缀(叠了反而会让 \\ 变成两个反斜杠)。
|
||||
lines = [
|
||||
_PATCH_MARKER,
|
||||
"import os as _ulab_os",
|
||||
f"_ulab_p = {lib_bin!r}",
|
||||
'if hasattr(_ulab_os, "add_dll_directory") and _ulab_os.path.isdir(_ulab_p):',
|
||||
" try: _UNILAB_DLL_HANDLE = _ulab_os.add_dll_directory(_ulab_p)",
|
||||
" except Exception: _UNILAB_DLL_HANDLE = None",
|
||||
]
|
||||
if preload_pyd:
|
||||
lines.extend(
|
||||
[
|
||||
"import ctypes as _ulab_ctypes",
|
||||
f"try: _ulab_ctypes.CDLL({preload_pyd!r})",
|
||||
"except Exception: pass",
|
||||
]
|
||||
)
|
||||
lines.append(_PATCH_END_MARKER)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _apply_dll_patch(file_path: str, lib_bin: str, preload_pyd: str = "") -> bool:
|
||||
"""把 DLL 补丁前置到 ``file_path``。文件不存在或已打过补丁则返回 False。"""
|
||||
if not os.path.isfile(file_path):
|
||||
return False
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
if _PATCH_MARKER in content:
|
||||
return False
|
||||
shutil.copy2(file_path, file_path + ".bak")
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(_build_dll_patch(lib_bin, preload_pyd) + content)
|
||||
return True
|
||||
|
||||
|
||||
def _print_restart_banner(patched_files):
|
||||
"""打印重启提示并以 EX_TEMPFAIL 退出。
|
||||
|
||||
- 不使用 ANSI 颜色码:Windows 旧版 cmd / PowerShell 5 默认不开 VT 处理,
|
||||
会把 ``\\033[1;33m`` 当做字面字符显示,反而让用户看不到正文。
|
||||
- 同时写入 stderr 与 stdout:某些上层 launcher / supervisor 只重定向
|
||||
其中一路,写两遍能保证用户至少看到一份。
|
||||
- 写入前防御性把流切到 UTF-8 with replace:``main.py`` 里已经做过一次,
|
||||
但本模块也可能被绕过 ``main.py`` 的代码路径直接 import;reconfigure
|
||||
失败也只是退回 errors=replace,不影响整体流程。
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
for _stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined]
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
|
||||
bar = "#" * 78
|
||||
files_lines = [f"[UniLabOS] - {p}" for p in patched_files]
|
||||
body = "\n".join(
|
||||
[
|
||||
"",
|
||||
bar,
|
||||
bar,
|
||||
"##",
|
||||
"## [UniLabOS] Windows + conda 下检测到 DLL 加载失败,已自动打补丁。",
|
||||
"## [UniLabOS] DLL load failure detected on Windows + conda;",
|
||||
"## [UniLabOS] the following files have been auto-patched:",
|
||||
"##",
|
||||
*[f"## {line}" for line in files_lines],
|
||||
"##",
|
||||
"## [UniLabOS] 当前进程的 rclpy 状态已损坏,补丁需要在新进程才生效。",
|
||||
"## [UniLabOS] The current process is unusable; the patch only takes",
|
||||
"## [UniLabOS] effect on a fresh process.",
|
||||
"##",
|
||||
"## >>> 请重新运行刚才的命令 / Please re-run the same command. <<<",
|
||||
"##",
|
||||
bar,
|
||||
bar,
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
for stream in (sys.stderr, sys.stdout):
|
||||
try:
|
||||
stream.write(body)
|
||||
stream.flush()
|
||||
except Exception:
|
||||
try:
|
||||
print(body, file=stream)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
sys.exit(_RESTART_EXIT_CODE)
|
||||
|
||||
|
||||
def patch_rclpy_dll_windows():
|
||||
"""在 Windows + conda 环境下修复 rclpy / rosidl typesupport 的 DLL 加载。
|
||||
|
||||
背景:conda 安装的 ros 系列包,其原生扩展依赖 ``$CONDA_PREFIX/Library/bin``
|
||||
下的 DLL;只有 conda 环境被正确激活、且 PATH 中含 ``Library/bin`` 时,
|
||||
``os.add_dll_directory`` 才能找到它们。当从快捷方式 / IDE / 子进程 /
|
||||
没激活的 shell 启动 ``unilab`` 时,会出现 ``DLL load failed``。
|
||||
|
||||
本函数会:
|
||||
1) 修补 ``rclpy/impl/implementation_singleton.py`` —— rclpy 自身的 C 扩展入口;
|
||||
2) 修补 ``rpyutils/add_dll_directories.py`` —— 所有 ``*_s__rosidl_typesupport_c.pyd``
|
||||
(``geometry_msgs`` / ``std_msgs`` / ``sensor_msgs`` 等)的统一加载入口。
|
||||
|
||||
打完补丁后**必须重启进程**才能生效(当前进程的 rclpy 已经发生过
|
||||
``ImportError``,子模块仍处于损坏状态)。因此函数会主动退出,并在
|
||||
stdout/stderr 同时打印明显的重启提示,避免用户被后续报错淹没。
|
||||
"""
|
||||
"""在 Windows + conda 环境下为 rclpy 打 DLL 加载补丁"""
|
||||
if sys.platform != "win32" or not os.environ.get("CONDA_PREFIX"):
|
||||
return
|
||||
|
||||
try:
|
||||
import rclpy # noqa: F401
|
||||
import rclpy
|
||||
|
||||
return
|
||||
except ImportError as e:
|
||||
if not str(e).startswith("DLL load failed"):
|
||||
return
|
||||
|
||||
cp = os.environ["CONDA_PREFIX"]
|
||||
lib_bin = os.path.join(cp, "Library", "bin")
|
||||
site_packages = os.path.join(cp, "Lib", "site-packages")
|
||||
if not os.path.isdir(lib_bin):
|
||||
impl = os.path.join(cp, "Lib", "site-packages", "rclpy", "impl", "implementation_singleton.py")
|
||||
pyd = glob.glob(os.path.join(cp, "Lib", "site-packages", "rclpy", "_rclpy_pybind11*.pyd"))
|
||||
if not os.path.exists(impl) or not pyd:
|
||||
return
|
||||
|
||||
patched = []
|
||||
|
||||
# 1) rclpy 自身的入口
|
||||
rclpy_impl = os.path.join(site_packages, "rclpy", "impl", "implementation_singleton.py")
|
||||
rclpy_pyd_matches = glob.glob(os.path.join(site_packages, "rclpy", "_rclpy_pybind11*.pyd"))
|
||||
rclpy_pyd = rclpy_pyd_matches[0] if rclpy_pyd_matches else ""
|
||||
if rclpy_pyd and _apply_dll_patch(rclpy_impl, lib_bin, preload_pyd=rclpy_pyd):
|
||||
patched.append(rclpy_impl)
|
||||
|
||||
# 2) rpyutils —— 所有 rosidl typesupport pyd 的加载点;放在 rclpy 之后
|
||||
# 例:geometry_msgs/geometry_msgs_s__rosidl_typesupport_c.pyd
|
||||
rpyutils_dll = os.path.join(site_packages, "rpyutils", "add_dll_directories.py")
|
||||
if _apply_dll_patch(rpyutils_dll, lib_bin):
|
||||
patched.append(rpyutils_dll)
|
||||
|
||||
if not patched:
|
||||
# 已经打过补丁但 rclpy 仍然加载失败:原因不是缺 DLL 搜索路径,
|
||||
# 不要再次打补丁污染文件,让上层看到真实的 ImportError。
|
||||
return
|
||||
|
||||
_print_restart_banner(patched)
|
||||
with open(impl, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
lib_bin = os.path.join(cp, "Library", "bin").replace("\\", "/")
|
||||
patch = f'# UniLabOS DLL Patch\nimport os,ctypes\nos.add_dll_directory("{lib_bin}") if hasattr(os,"add_dll_directory") else None\ntry: ctypes.CDLL("{pyd[0].replace(chr(92),"/")}")\nexcept: pass\n# End Patch\n'
|
||||
shutil.copy2(impl, impl + ".bak")
|
||||
with open(impl, "w", encoding="utf-8") as f:
|
||||
f.write(patch + content)
|
||||
|
||||
|
||||
patch_rclpy_dll_windows()
|
||||
|
||||
@@ -58,14 +58,14 @@ class JobResultStore:
|
||||
feedback=feedback or {},
|
||||
timestamp=time.time(),
|
||||
)
|
||||
logger.trace(f"[JobResultStore] Stored result for job {job_id[:8]}, status={status}")
|
||||
logger.debug(f"[JobResultStore] Stored result for job {job_id[:8]}, status={status}")
|
||||
|
||||
def get_and_remove(self, job_id: str) -> Optional[JobResult]:
|
||||
"""获取并删除任务结果"""
|
||||
with self._results_lock:
|
||||
result = self._results.pop(job_id, None)
|
||||
if result:
|
||||
logger.trace(f"[JobResultStore] Retrieved and removed result for job {job_id[:8]}")
|
||||
logger.debug(f"[JobResultStore] Retrieved and removed result for job {job_id[:8]}")
|
||||
return result
|
||||
|
||||
def get_result(self, job_id: str) -> Optional[JobResult]:
|
||||
@@ -320,7 +320,6 @@ def job_add(req: JobAddReq) -> JobData:
|
||||
action_name=action_name,
|
||||
task_id=task_id,
|
||||
job_id=job_id,
|
||||
notebook_id=req.notebook_id,
|
||||
device_action_key=device_action_key,
|
||||
)
|
||||
|
||||
|
||||
@@ -59,7 +59,6 @@ class QueueItem:
|
||||
action_name: str
|
||||
task_id: str
|
||||
job_id: str
|
||||
notebook_id: str
|
||||
device_action_key: str
|
||||
next_run_time: float = 0 # 下次执行时间戳
|
||||
retry_count: int = 0 # 重试次数
|
||||
@@ -72,7 +71,6 @@ class JobInfo:
|
||||
job_id: str
|
||||
task_id: str
|
||||
device_id: str
|
||||
notebook_id: str
|
||||
action_name: str
|
||||
device_action_key: str
|
||||
status: JobStatus
|
||||
@@ -541,10 +539,7 @@ class MessageProcessor:
|
||||
self.reconnect_count += 1
|
||||
backoff = WSConfig.reconnect_interval
|
||||
logger.info(
|
||||
"[MessageProcessor] 即将在 %s 秒后重连 (已尝试 %s/%s)",
|
||||
backoff,
|
||||
self.reconnect_count,
|
||||
WSConfig.max_reconnect_attempts,
|
||||
f"[MessageProcessor] 即将在 {backoff} 秒后重连 (已尝试 {self.reconnect_count}/{WSConfig.max_reconnect_attempts})"
|
||||
)
|
||||
await asyncio.sleep(backoff)
|
||||
else:
|
||||
@@ -708,7 +703,6 @@ class MessageProcessor:
|
||||
action_name = data.get("action_name", "")
|
||||
task_id = data.get("task_id", "")
|
||||
job_id = data.get("job_id", "")
|
||||
notebook_id = data.get("notebook_id", "")
|
||||
|
||||
if not all([device_id, action_name, task_id, job_id]):
|
||||
logger.error("[MessageProcessor] Missing required fields in query_action_state")
|
||||
@@ -724,7 +718,6 @@ class MessageProcessor:
|
||||
job_id=job_id,
|
||||
task_id=task_id,
|
||||
device_id=device_id,
|
||||
notebook_id=notebook_id,
|
||||
action_name=action_name,
|
||||
device_action_key=device_action_key,
|
||||
status=JobStatus.QUEUE,
|
||||
@@ -739,27 +732,13 @@ class MessageProcessor:
|
||||
if can_start_immediately:
|
||||
# 可以立即开始
|
||||
await self._send_action_state_response(
|
||||
device_id,
|
||||
action_name,
|
||||
task_id,
|
||||
job_id,
|
||||
"query_action_status",
|
||||
True,
|
||||
0,
|
||||
notebook_id=notebook_id,
|
||||
device_id, action_name, task_id, job_id, "query_action_status", True, 0
|
||||
)
|
||||
logger.trace(f"[MessageProcessor] Job {job_log} can start immediately")
|
||||
else:
|
||||
# 需要排队
|
||||
await self._send_action_state_response(
|
||||
device_id,
|
||||
action_name,
|
||||
task_id,
|
||||
job_id,
|
||||
"query_action_status",
|
||||
False,
|
||||
10,
|
||||
notebook_id=notebook_id,
|
||||
device_id, action_name, task_id, job_id, "query_action_status", False, 10
|
||||
)
|
||||
logger.trace(f"[MessageProcessor] Job {job_log} queued")
|
||||
|
||||
@@ -789,7 +768,6 @@ class MessageProcessor:
|
||||
job_id=req.job_id,
|
||||
task_id=req.task_id,
|
||||
device_id=req.device_id,
|
||||
notebook_id=req.notebook_id,
|
||||
action_name=action_name,
|
||||
device_action_key=device_action_key,
|
||||
status=JobStatus.QUEUE,
|
||||
@@ -797,16 +775,11 @@ class MessageProcessor:
|
||||
always_free=True,
|
||||
)
|
||||
self.device_manager.add_queue_request(job_info)
|
||||
existing_job = job_info
|
||||
logger.info(f"[MessageProcessor] Job {job_log} always_free, auto-registered from direct job_start")
|
||||
else:
|
||||
logger.error(f"[MessageProcessor] Job {job_log} not registered (missing query_action_state)")
|
||||
return
|
||||
|
||||
if existing_job and req.notebook_id and not existing_job.notebook_id:
|
||||
existing_job.notebook_id = req.notebook_id
|
||||
notebook_id = req.notebook_id or (existing_job.notebook_id if existing_job else "")
|
||||
|
||||
success = self.device_manager.start_job(req.job_id)
|
||||
if not success:
|
||||
logger.error(f"[MessageProcessor] Failed to start job {job_log}")
|
||||
@@ -822,7 +795,6 @@ class MessageProcessor:
|
||||
action_name=req.action,
|
||||
task_id=req.task_id,
|
||||
job_id=req.job_id,
|
||||
notebook_id=notebook_id,
|
||||
device_action_key=device_action_key,
|
||||
)
|
||||
|
||||
@@ -862,7 +834,6 @@ class MessageProcessor:
|
||||
"job_id": req.job_id,
|
||||
"task_id": req.task_id,
|
||||
"device_id": req.device_id,
|
||||
"notebook_id": queue_item.notebook_id,
|
||||
"action_name": req.action,
|
||||
"status": "failed",
|
||||
"feedback_data": {},
|
||||
@@ -884,7 +855,6 @@ class MessageProcessor:
|
||||
"query_action_status",
|
||||
True,
|
||||
0,
|
||||
notebook_id=next_job.notebook_id,
|
||||
)
|
||||
next_job_log = format_job_log(
|
||||
next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
||||
@@ -1131,15 +1101,7 @@ class MessageProcessor:
|
||||
logger.info(f"[MessageProcessor] Restart cleanup scheduled")
|
||||
|
||||
async def _send_action_state_response(
|
||||
self,
|
||||
device_id: str,
|
||||
action_name: str,
|
||||
task_id: str,
|
||||
job_id: str,
|
||||
typ: str,
|
||||
free: bool,
|
||||
need_more: int,
|
||||
notebook_id: str = "",
|
||||
self, device_id: str, action_name: str, task_id: str, job_id: str, typ: str, free: bool, need_more: int
|
||||
):
|
||||
"""发送动作状态响应"""
|
||||
message = {
|
||||
@@ -1150,7 +1112,6 @@ class MessageProcessor:
|
||||
"action_name": action_name,
|
||||
"task_id": task_id,
|
||||
"job_id": job_id,
|
||||
"notebook_id": notebook_id,
|
||||
"free": free,
|
||||
"need_more": need_more + 1,
|
||||
},
|
||||
@@ -1233,7 +1194,6 @@ class QueueProcessor:
|
||||
action_name=timeout_job.action_name,
|
||||
task_id=timeout_job.task_id,
|
||||
job_id=timeout_job.job_id,
|
||||
notebook_id=timeout_job.notebook_id,
|
||||
device_action_key=timeout_job.device_action_key,
|
||||
)
|
||||
# 发布超时失败状态,这会触发正常的job完成流程
|
||||
@@ -1292,7 +1252,6 @@ class QueueProcessor:
|
||||
"action_name": job_info.action_name,
|
||||
"task_id": job_info.task_id,
|
||||
"job_id": job_info.job_id,
|
||||
"notebook_id": job_info.notebook_id,
|
||||
"free": False,
|
||||
"need_more": 10 + 1,
|
||||
},
|
||||
@@ -1332,7 +1291,6 @@ class QueueProcessor:
|
||||
"action_name": job_info.action_name,
|
||||
"task_id": job_info.task_id,
|
||||
"job_id": job_info.job_id,
|
||||
"notebook_id": job_info.notebook_id,
|
||||
"free": False,
|
||||
"need_more": 10 + 1,
|
||||
},
|
||||
@@ -1378,15 +1336,12 @@ class QueueProcessor:
|
||||
"action_name": next_job.action_name,
|
||||
"task_id": next_job.task_id,
|
||||
"job_id": next_job.job_id,
|
||||
"notebook_id": next_job.notebook_id,
|
||||
"free": True,
|
||||
"need_more": 0,
|
||||
},
|
||||
}
|
||||
self.message_processor.send_message(message)
|
||||
# next_job_log = format_job_log(
|
||||
# next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name
|
||||
# )
|
||||
# next_job_log = format_job_log(next_job.job_id, next_job.task_id, next_job.device_id, next_job.action_name)
|
||||
# logger.debug(f"[QueueProcessor] Notified next job {next_job_log} can start")
|
||||
|
||||
# 立即触发下一轮状态检查
|
||||
@@ -1555,7 +1510,6 @@ class WebSocketClient(BaseCommunicationClient):
|
||||
"job_id": item.job_id,
|
||||
"task_id": item.task_id,
|
||||
"device_id": item.device_id,
|
||||
"notebook_id": item.notebook_id,
|
||||
"action_name": item.action_name,
|
||||
"status": status,
|
||||
"feedback_data": feedback_data,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -201,17 +201,42 @@ class ResourceVisualization:
|
||||
self.moveit_controllers_yaml['moveit_simple_controller_manager'][f"{name}_{controller_name}"] = moveit_dict['moveit_simple_controller_manager'][controller_name]
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _ensure_ros2_env() -> dict:
|
||||
"""确保 ROS2 环境变量正确设置,返回可用于子进程的 env dict"""
|
||||
import sys
|
||||
env = dict(os.environ)
|
||||
conda_prefix = os.path.dirname(os.path.dirname(sys.executable))
|
||||
|
||||
if "AMENT_PREFIX_PATH" not in env or not env["AMENT_PREFIX_PATH"].strip():
|
||||
candidate = os.pathsep.join([conda_prefix, os.path.join(conda_prefix, "Library")])
|
||||
env["AMENT_PREFIX_PATH"] = candidate
|
||||
os.environ["AMENT_PREFIX_PATH"] = candidate
|
||||
|
||||
extra_bin_dirs = [
|
||||
os.path.join(conda_prefix, "Library", "bin"),
|
||||
os.path.join(conda_prefix, "Library", "lib"),
|
||||
os.path.join(conda_prefix, "Scripts"),
|
||||
conda_prefix,
|
||||
]
|
||||
current_path = env.get("PATH", "")
|
||||
for d in extra_bin_dirs:
|
||||
if d not in current_path:
|
||||
current_path = d + os.pathsep + current_path
|
||||
env["PATH"] = current_path
|
||||
os.environ["PATH"] = current_path
|
||||
|
||||
return env
|
||||
|
||||
def create_launch_description(self) -> LaunchDescription:
|
||||
"""
|
||||
创建launch描述,包含robot_state_publisher和move_group节点
|
||||
|
||||
Args:
|
||||
urdf_str: URDF文本
|
||||
|
||||
Returns:
|
||||
LaunchDescription: launch描述对象
|
||||
"""
|
||||
# 检查ROS 2环境变量
|
||||
launch_env = self._ensure_ros2_env()
|
||||
|
||||
if "AMENT_PREFIX_PATH" not in os.environ:
|
||||
raise OSError(
|
||||
"ROS 2环境未正确设置。需要设置 AMENT_PREFIX_PATH 环境变量。\n"
|
||||
@@ -290,7 +315,7 @@ class ResourceVisualization:
|
||||
{"robot_description": robot_description},
|
||||
ros2_controllers,
|
||||
],
|
||||
env=dict(os.environ)
|
||||
env=launch_env,
|
||||
)
|
||||
)
|
||||
for controller in self.moveit_controllers_yaml['moveit_simple_controller_manager']['controller_names']:
|
||||
@@ -300,7 +325,7 @@ class ResourceVisualization:
|
||||
executable="spawner",
|
||||
arguments=[f"{controller}", "--controller-manager", f"controller_manager"],
|
||||
output="screen",
|
||||
env=dict(os.environ)
|
||||
env=launch_env,
|
||||
)
|
||||
)
|
||||
controllers.append(
|
||||
@@ -309,7 +334,7 @@ class ResourceVisualization:
|
||||
executable="spawner",
|
||||
arguments=["joint_state_broadcaster", "--controller-manager", f"controller_manager"],
|
||||
output="screen",
|
||||
env=dict(os.environ)
|
||||
env=launch_env,
|
||||
)
|
||||
)
|
||||
for i in controllers:
|
||||
@@ -317,7 +342,6 @@ class ResourceVisualization:
|
||||
else:
|
||||
ros2_controllers = None
|
||||
|
||||
# 创建robot_state_publisher节点
|
||||
robot_state_publisher = nd(
|
||||
package='robot_state_publisher',
|
||||
executable='robot_state_publisher',
|
||||
@@ -327,9 +351,8 @@ class ResourceVisualization:
|
||||
'robot_description': robot_description,
|
||||
'use_sim_time': False
|
||||
},
|
||||
# kinematics_dict
|
||||
],
|
||||
env=dict(os.environ)
|
||||
env=launch_env,
|
||||
)
|
||||
|
||||
|
||||
@@ -361,7 +384,7 @@ class ResourceVisualization:
|
||||
executable='move_group',
|
||||
output='screen',
|
||||
parameters=moveit_params,
|
||||
env=dict(os.environ)
|
||||
env=launch_env,
|
||||
)
|
||||
|
||||
|
||||
@@ -379,13 +402,11 @@ class ResourceVisualization:
|
||||
arguments=['-d', f"{str(self.mesh_path)}/view_robot.rviz"],
|
||||
output='screen',
|
||||
parameters=[
|
||||
{'robot_description_kinematics': kinematics_dict,
|
||||
},
|
||||
{'robot_description_kinematics': kinematics_dict},
|
||||
robot_description_planning,
|
||||
planning_pipelines,
|
||||
|
||||
],
|
||||
env=dict(os.environ)
|
||||
env=launch_env,
|
||||
)
|
||||
self.launch_description.add_action(rviz_node)
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
# Donghua EC 用户指南(UniLab 接入版)
|
||||
|
||||
## 概述
|
||||
|
||||
- 提供两套使用方式:
|
||||
- 测试封装动作:一条指令完成“启动实验 → 实时采样写文件 → 导出数据”,可选自动停止。
|
||||
- 基础启动动作:按需组合“启动实验、实时输出、停止、导出”,更灵活可编排。
|
||||
|
||||
## 设备配置
|
||||
|
||||
- `interface_dir`:DHInterface 目录(包含 `ECCore.dll` 与配置文件),示例:`d:\Uni-Lab-OS\Uni-Lab-OS\unilabos\devices\donghua_ec\x64release\DHInterface`(注册见 `unilabos/registry/devices/donghua_ec.yaml:1940`)。
|
||||
- `dll_path`(可选):若与 `interface_dir` 不一致,可直接指定 `ECCore.dll` 完整路径(`donghua_ec.yaml:1936`)。
|
||||
- 默认通道:`machine_id`(`donghua_ec.yaml:1944`)。
|
||||
|
||||
## 初始化
|
||||
|
||||
- 后端自动:设备注册后会自动调用 `auto-initialize`(加载 DLL)与 `auto-post_init`(注入),无需前端干预(`donghua_ec.yaml:27`、`donghua_ec.yaml:48`)。
|
||||
- 手动(可选):
|
||||
- 调用 `auto-initialize`:`{"device_id":"<设备ID>","action":"auto-initialize"}`
|
||||
- 调用 `auto-post_init`:`{"device_id":"<设备ID>","action":"auto-post_init"}`
|
||||
|
||||
## 动作总览
|
||||
|
||||
- 测试封装动作(均要求传入 `output_dir`):
|
||||
- `test_open_circuit_energy`(默认 `stop_after=true`,使用轮询检测实验结束后再停止与导出,不再使用 `wait_seconds`)
|
||||
- `test_eis`(默认 `stop_after=false`,避免提前结束,`donghua_ec.yaml:1480`)
|
||||
- `test_gitt`(默认 `stop_after=false`,`donghua_ec.yaml:1627`)
|
||||
- `test_linear_scan_voltammetry`(默认 `stop_after=false`,必填 `output_dir`,参考 `donghua_ec.yaml:1750` 及后续)
|
||||
- 基础启动与组合:
|
||||
- `start_open_circuit_energy`、`start_eis`、`start_gitt`、`start_linear_scan_voltammetry`
|
||||
- 实时输出:`start_realtime_output` / `stop_realtime_output`(`donghua_ec.yaml:1068`、`donghua_ec.yaml:1155`)
|
||||
- 停止实验:`stop_experiment`(`donghua_ec.yaml:1118`)
|
||||
- 导出数据:`export_*_data`(如 `export_eis_data`、`export_gitt_data` 等,均要求 `output_dir/dest_dir`)
|
||||
|
||||
## 快速测试流程(推荐)
|
||||
|
||||
- 开路电位:
|
||||
- 请求:
|
||||
```json
|
||||
{"device_id":"<设备ID>","action":"test_open_circuit_energy","action_args":{"output_dir":"d:/data/oc","interval":0.5,"stop_after":true}}
|
||||
```
|
||||
- 返回包含:`success`、`realtime_file`、`export_files`、`export_dest`。
|
||||
- 阻抗(EIS):
|
||||
- 请求(只需给导出目录,其他用默认即可):
|
||||
```json
|
||||
{"device_id":"<设备ID>","action":"test_eis","action_args":{"output_dir":"d:/data/eis","start_freq":10000,"end_freq":0.1,"amplitude":0.01,"point_count":10,"interval":0.5}}
|
||||
```
|
||||
- 默认不自动停止(`stop_after=false`),可在完成采样后继续扫频;若需自动停,传 `stop_after=true`。
|
||||
- GITT:
|
||||
- 请求:
|
||||
```json
|
||||
{"device_id":"<设备ID>","action":"test_gitt","action_args":{"output_dir":"d:/data/gitt","current":1.0,"time_per_point_cc":0.1,"continue_time_cc":60,"time_per_point_oc":0.1,"continue_time_oc":60,"is_voltage_trig":true,"voltage_or_current_trig_direction":0,"voltage_or_current_trig_value":0,"interval":0.5}}
|
||||
```
|
||||
|
||||
## 基础启动与组合(灵活编排)
|
||||
|
||||
- 启动 EIS:
|
||||
- `{"device_id":"<设备ID>","action":"start_eis","action_args":{"start_freq":10000,"end_freq":0.1,"amplitude":0.01,"point_count":10}}`
|
||||
- 开启实时输出:
|
||||
- `{"device_id":"<设备ID>","action":"start_realtime_output","action_args":{"interval":0.5}}`
|
||||
- 关闭实时输出并获取文件:
|
||||
- `{"device_id":"<设备ID>","action":"stop_realtime_output"}`
|
||||
- 导出数据到目录:
|
||||
- `{"device_id":"<设备ID>","action":"export_eis_data","action_args":{"output_dir":"d:/data/eis"}}`
|
||||
- 停止实验(可选):
|
||||
- `{"device_id":"<设备ID>","action":"stop_experiment"}`
|
||||
|
||||
## 重要说明
|
||||
|
||||
- 必填导出目录:所有 `test_*` 和 `export_*` 动作需要提供 `output_dir`(或 `dest_dir`),否则不会复制数据到目标位置(`donghua_ec.yaml:1545`、`donghua_ec.yaml:1710`)。
|
||||
- 关于提前结束:非开路的测试封装动作默认 `stop_after=false`,避免在实时采样后调用 `stop_experiment`,从而导致频率扫描未达到 `end_freq` 就停止(修复见 `donghua_ec.yaml:1480`、`donghua_ec.yaml:1627`)。
|
||||
- 实时文件位置:若未指定 `dest_dir`,实时输出会写入 `interface_dir/SourceData/<日期>/<实验子目录>`(实现参考 `unilabos/devices/donghua_ec/donghua_ec.py:1042`)。
|
||||
|
||||
## 数据字段(参考)
|
||||
|
||||
- EIS 拆分:`time/zre/zim/z/freq/phase/edc`(实现参考 `unilabos/devices/donghua_ec/donghua_ec.py:1109`)。
|
||||
- 线性扫描与循环伏安:`time/potential/current` 等(实现参考 `donghua_ec.py:1111`、`donghua_ec.py:1114`)。
|
||||
- 开路电位:写入时间序列与电位(`donghua_ec.py:1045`)。
|
||||
@@ -1,3 +0,0 @@
|
||||
from .donghua_ec import DonghuaEC
|
||||
|
||||
__all__ = ["DonghuaEC"]
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "donghua_ec_device",
|
||||
"name": "Donghua_EC",
|
||||
"parent": null,
|
||||
"type": "device",
|
||||
"class": "donghua_ec",
|
||||
"position": {
|
||||
"x": 620.6111111111111,
|
||||
"y": 171,
|
||||
"z": 0
|
||||
},
|
||||
"config": {
|
||||
"interface_dir": "D:/Uni-Lab-OS/Uni-Lab-OS/unilabos/devices/donghua_ec/x64release/DHInterface",
|
||||
"dll_path": "",
|
||||
"machine_id": 0
|
||||
},
|
||||
"data": {},
|
||||
"children": []
|
||||
}
|
||||
],
|
||||
"links": []
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,358 +0,0 @@
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
@@ -1,398 +0,0 @@
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 1992.742065 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 1992.826050 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 1993.161743 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 1993.077881 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 1992.154907 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 1992.909912 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 1993.077881 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 1993.161743 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 1992.826050 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 1993.161743 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 1992.742065 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 1992.993896 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 1992.742065 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 1992.909912 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 1993.077881 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 1993.916748 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 1994.000732 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 1993.413452 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 1992.993896 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 1993.581177 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 1993.413452 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 1993.497314 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 1993.161743 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 1993.916748 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 1993.749023 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 1994.000732 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 1994.084717 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 1993.749023 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 1993.916748 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 1993.497314 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 1993.749023 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 1993.413452 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 1993.749023 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 1994.168579 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 1993.916748 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 1993.497314 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 1994.084717 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 1994.084717 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 1994.420288 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 1994.504150 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 1994.168579 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 1994.336426 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 1994.504150 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 1994.923706 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 1995.427246 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 1994.923706 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 1995.678833 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 1995.762817 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 1994.923706 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 1996.685791 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 1995.762817 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 1994.420288 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 1993.329590 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 1993.077881 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 1992.742065 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 1993.245605 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 1993.329590 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 1994.336426 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 1995.343262 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 1995.427246 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 1995.594971 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.100000 1994.504150 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.200000 1994.336426 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.300000 1994.000732 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.400000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.500000 1993.329590 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.600000 1994.000732 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.700000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.800000 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.900000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.000000 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.100000 1994.923706 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.200000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.300000 1996.014404 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.400000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.500000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.600000 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.700000 1995.343262 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.800000 1996.014404 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.900000 1995.762817 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.000000 1995.678833 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.100000 1996.434082 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.200000 1996.098389 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.300000 1995.343262 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.400000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.500000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.600000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.700000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.800000 1995.678833 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.900000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.000000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.100000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.200000 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.300000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.400000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.500000 1995.762817 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.600000 1995.930542 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.700000 1995.343262 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.800000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.900000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.000000 1995.678833 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.100000 1996.182251 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.200000 1996.434082 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.300000 1995.846680 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.400000 1996.350220 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.500000 1996.098389 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.600000 1995.343262 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.700000 1995.427246 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.800000 1995.427246 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.900000 1995.511108 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.000000 1995.678833 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.100000 1995.343262 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.200000 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.300000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.400000 1995.427246 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.500000 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.600000 1995.175415 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.700000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.800000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.900000 1993.665161 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.000000 1994.084717 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.100000 1994.168579 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.200001 1994.252563 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.299999 1994.839722 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.400000 1996.350220 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.500000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.600000 1994.588135 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.700001 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.799999 1995.259399 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.900000 1995.427246 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.000000 1995.427246 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.100000 1995.091431 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.200001 1994.839722 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.299999 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.400000 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.500000 1994.588135 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.600000 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.700001 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.799999 1994.420288 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.900000 1994.336426 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.000000 1994.420288 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.100000 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.200001 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.299999 1994.923706 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.400000 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.500000 1994.839722 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.600000 1993.832886 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.700001 1994.588135 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.799999 1994.839722 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.900000 1994.839722 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.000000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.100000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.200001 1994.588135 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.299999 1994.420288 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.400000 1994.336426 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.500000 1994.336426 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.600000 1994.671997 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.700001 1994.755859 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.799999 1995.007568 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 2057.013672 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 2057.684814 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 2057.936523 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 2058.020508 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 2058.523682 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 2058.775635 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 2058.775635 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 2058.859619 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 2059.362793 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 2059.279053 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 2059.446777 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 2060.034180 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 2060.285889 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 2060.201904 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 2060.537598 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 2060.789307 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 2061.041016 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 2061.125000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 2061.628418 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 2061.880127 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 2061.963867 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 2062.047852 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 2062.383545 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 2062.718994 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 2062.718994 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 2062.970703 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 2063.306396 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 2063.306396 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 2063.558105 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 2063.642090 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 2064.061523 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 2064.061523 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 2064.229248 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 2064.229248 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 2064.732910 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 2064.732910 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 2065.236328 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 2065.152344 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 2065.404053 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 2065.320068 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 2065.739746 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 2065.907471 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 2065.991455 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 2065.991455 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 2065.991455 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 2066.326904 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 2066.830566 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 2066.578613 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 2066.998291 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 2067.333984 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 2067.585449 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 2067.585449 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 2067.669434 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 2067.837402 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 2067.921387 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 2068.173096 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 2068.340820 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 2068.592529 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 2068.760254 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 2068.592529 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 2068.760254 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 2069.095947 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 2069.095947 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 2069.011963 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 2069.683105 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 2069.599365 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 2069.767090 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 2069.767090 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 2069.851074 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 2070.186768 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 2069.851074 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 2070.354492 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 2070.186768 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 2070.606201 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 2070.773926 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 2070.606201 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 2071.109619 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 2070.773926 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 2071.109619 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 2071.025635 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 2071.361328 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 2071.445312 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 2071.445312 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 2071.529297 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 2071.864746 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 2072.116455 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 2071.948730 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 2071.948730 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 2072.284424 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 2072.368164 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 2072.452148 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 2072.619873 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 2072.955566 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 2072.871582 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 2072.955566 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 2073.207275 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 2073.207275 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 2073.458984 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 2073.291016 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 2073.458984 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.100000 2073.458984 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.200000 2073.878418 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.300000 2073.794678 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.400000 2074.046387 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.500000 2074.046387 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.600000 2073.962402 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.700000 2074.214111 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.800000 2074.214111 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.900000 2074.130127 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.000000 2074.717529 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.100000 2074.549805 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.200000 2074.801514 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.300000 2074.885498 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.400000 2074.885498 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.500000 2074.801514 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.600000 2075.137207 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.700000 2075.137207 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.800000 2075.220947 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.900000 2075.304932 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.000000 2075.556641 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.100000 2075.388672 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.200000 2075.808350 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.300000 2075.808350 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.400000 2075.808350 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.500000 2075.892334 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.600000 2075.892334 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.700000 2075.976074 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.800000 2076.144043 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.900000 2076.060059 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.000000 2076.395752 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.100000 2076.395752 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.200000 2076.479492 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.300000 2076.563477 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.400000 2076.563477 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.500000 2076.731201 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.600000 2076.983154 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.700000 2077.150879 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.800000 2077.066895 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.900000 2077.402588 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.000000 2077.318604 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.100000 2077.402588 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.200000 2077.486328 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.300000 2077.570312 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.400000 2077.570312 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.500000 2078.073730 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.600000 2077.906006 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.700000 2077.822021 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.800000 2078.073730 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.900000 2078.157715 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.000000 2078.157715 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.100000 2078.241699 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.200000 2078.409424 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.300000 2078.409424 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.400000 2078.661133 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.500000 2078.828857 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.600000 2078.577148 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.700000 2078.912842 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.800000 2078.996582 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.900000 2078.996582 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.000000 2079.248535 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.100000 2079.332275 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.200001 2079.248535 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.299999 2079.500244 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.400000 2079.667969 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.500000 2079.835693 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.600000 2079.835693 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.700001 2079.583984 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.799999 2079.919678 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.900000 2080.171387 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.000000 2080.003662 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.100000 2080.087402 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.200001 2080.171387 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.299999 2080.003662 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.400000 2080.255371 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.500000 2080.507080 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.600000 2080.674805 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.700001 2080.758789 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.799999 2080.591064 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.900000 2080.758789 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.000000 2080.926514 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.100000 2080.926514 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.200001 2081.094238 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.299999 2081.262207 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.400000 2081.346191 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.500000 2081.429932 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.600000 2081.262207 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.700001 2081.597900 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.799999 2081.681641 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.900000 2081.513916 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.000000 2081.681641 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.100000 2082.101318 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.200001 2082.017334 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.299999 2081.849609 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.400000 2082.017334 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.500000 2082.101318 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.600000 2082.352783 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.700001 2082.269043 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.799999 2082.604736 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
@@ -1,597 +0,0 @@
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
11.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
12.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
13.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
14.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
15.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
16.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
17.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
18.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.200001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.299999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.700001 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
19.799999 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
@@ -1,101 +0,0 @@
|
||||
Time(s) E(mV) I(mA) Q(mC) Capacity(mAh) Energy(Wh) P(W)
|
||||
0.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
0.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
1.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
2.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
3.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
4.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
5.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
6.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
7.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
8.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.200000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.300000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.400000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.500000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.600000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.700000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.800000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
9.900000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
10.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
|
||||
@@ -1,981 +0,0 @@
|
||||
Time(s) E(mV) I(mA)
|
||||
0.010000 -997.723450 -5.245421
|
||||
0.020000 -996.045349 -5.010738
|
||||
0.030000 -993.947693 -4.961776
|
||||
0.040000 -991.682251 -4.928009
|
||||
0.050000 -989.752441 -4.899306
|
||||
0.060000 -987.990417 -5.074349
|
||||
0.070000 -985.641052 -4.800336
|
||||
0.080000 -983.879028 -4.896254
|
||||
0.090000 -981.697510 -4.842570
|
||||
0.100000 -979.935486 -4.868352
|
||||
0.110000 -978.005676 -4.946120
|
||||
0.120000 -975.656372 -4.801778
|
||||
0.130000 -973.978271 -4.739869
|
||||
0.140000 -971.880615 -4.706879
|
||||
0.150000 -969.866882 -4.946629
|
||||
0.160000 -967.769287 -4.942389
|
||||
0.170000 -966.007263 -4.839263
|
||||
0.180000 -963.657898 -4.812040
|
||||
0.190000 -961.811951 -4.663457
|
||||
0.200000 -959.798279 -4.709678
|
||||
0.210000 -957.784485 -4.658539
|
||||
0.220000 -955.938599 -4.624955
|
||||
0.230000 -953.757080 -4.610198
|
||||
0.240000 -951.995056 -4.587724
|
||||
0.250000 -949.897400 -4.594509
|
||||
0.260000 -947.883728 -4.307436
|
||||
0.270000 -946.037842 -4.310320
|
||||
0.280000 -943.940186 -4.356200
|
||||
0.290000 -941.926453 -4.364511
|
||||
0.300000 -939.661011 -4.595781
|
||||
0.310000 -937.731201 -4.543455
|
||||
0.320000 -935.717407 -4.559314
|
||||
0.330000 -933.871521 -4.562452
|
||||
0.340000 -931.606079 -4.427014
|
||||
0.350000 -929.844116 -4.489263
|
||||
0.360000 -927.662537 -4.452202
|
||||
0.370000 -925.732788 -4.399452
|
||||
0.380000 -923.886780 -4.405304
|
||||
0.390000 -921.873108 -4.408611
|
||||
0.400000 -919.691589 -4.333557
|
||||
0.410000 -917.593933 -4.334066
|
||||
0.420000 -915.831909 -4.386562
|
||||
0.430000 -913.482544 -4.543200
|
||||
0.440000 -913.146973 -4.722992
|
||||
0.450000 -909.622925 -4.428881
|
||||
0.460000 -907.777039 -4.410053
|
||||
0.470000 -905.679382 -4.363664
|
||||
0.480000 -903.581787 -4.374688
|
||||
0.490000 -901.735840 -4.188112
|
||||
0.500000 -899.973816 -3.465724
|
||||
0.510000 -898.127930 -3.826409
|
||||
0.520000 -895.694702 -3.733291
|
||||
0.530000 -893.848755 -3.535859
|
||||
0.540000 -891.751160 -3.213846
|
||||
0.550000 -889.821289 -3.182298
|
||||
0.560000 -888.059265 -2.966124
|
||||
0.570000 -885.961731 -2.916851
|
||||
0.580000 -883.360596 -3.444437
|
||||
0.590000 -881.682495 -3.675622
|
||||
0.600000 -880.004395 -3.014973
|
||||
0.610000 -877.738953 -2.964597
|
||||
0.620000 -875.893066 -2.955438
|
||||
0.630000 -873.543701 -3.789857
|
||||
0.640000 -871.781738 -2.904723
|
||||
0.650000 -869.767944 -3.019892
|
||||
0.660000 -867.838196 -2.876482
|
||||
0.670000 -865.572754 -2.871055
|
||||
0.680000 -863.894653 -2.873005
|
||||
0.690000 -861.796997 -2.856468
|
||||
0.700000 -859.699341 -2.857570
|
||||
0.710000 -857.685669 -3.277282
|
||||
0.720000 -855.755798 -3.240815
|
||||
0.730000 -853.909912 -2.812538
|
||||
0.740000 -851.812256 -2.800071
|
||||
0.750000 -849.714661 -2.830602
|
||||
0.760000 -847.617004 -2.861980
|
||||
0.770000 -845.687134 -2.828651
|
||||
0.780000 -843.673462 -2.989106
|
||||
0.790000 -841.575806 -3.356577
|
||||
0.800000 -839.981567 -3.497866
|
||||
0.810000 -837.212769 -3.823356
|
||||
0.820000 -835.534668 -3.269310
|
||||
0.830000 -833.437012 -3.710393
|
||||
0.840000 -831.842834 -2.778360
|
||||
0.850000 -829.829041 -2.802276
|
||||
0.860000 -827.815369 -2.759279
|
||||
0.870000 -825.717712 -2.751392
|
||||
0.880000 -823.787903 -2.733328
|
||||
0.890000 -821.942017 -2.720352
|
||||
0.900000 -819.844360 -2.750289
|
||||
0.910000 -817.830627 -2.919310
|
||||
0.920000 -815.733032 -2.894377
|
||||
0.930000 -813.803162 -2.898023
|
||||
0.940000 -811.789490 -2.866390
|
||||
0.950000 -809.859619 -2.864864
|
||||
0.960000 -807.510315 -3.323078
|
||||
0.970000 -805.916077 -2.828821
|
||||
0.980000 -803.566772 -2.821018
|
||||
0.990000 -801.888611 -2.812368
|
||||
1.000000 -799.874939 -2.779124
|
||||
1.010000 -797.777283 -2.807873
|
||||
1.020000 -795.847534 -2.797611
|
||||
1.030000 -793.498108 -2.787180
|
||||
1.040000 -791.903992 -2.775138
|
||||
1.050000 -789.974121 -2.762417
|
||||
1.060000 -787.792542 -2.723151
|
||||
1.070000 -785.694946 -2.718826
|
||||
1.080000 -784.016846 -2.685411
|
||||
1.090000 -782.003113 -2.675913
|
||||
1.100000 -779.653748 -2.651997
|
||||
1.110000 -777.807861 -2.652761
|
||||
1.120000 -775.877991 -2.638598
|
||||
1.130000 -773.864319 -2.628506
|
||||
1.140000 -771.682800 -2.617651
|
||||
1.150000 -769.501221 -2.629269
|
||||
1.160000 -767.907043 -2.609424
|
||||
1.170000 -765.809387 -2.601622
|
||||
1.180000 -763.879578 -2.593480
|
||||
1.190000 -761.698059 -2.583049
|
||||
1.200000 -759.600464 -2.560490
|
||||
1.210000 -757.586731 -2.555995
|
||||
1.220000 -755.740784 -2.563713
|
||||
1.230000 -753.894836 -2.534794
|
||||
1.240000 -752.048950 -2.521224
|
||||
1.250000 -749.699585 -2.530044
|
||||
1.260000 -747.685913 -2.603827
|
||||
1.270000 -745.672180 -2.536320
|
||||
1.280000 -743.658386 -2.561847
|
||||
1.290000 -741.728638 -2.517239
|
||||
1.300000 -739.798828 -2.482468
|
||||
1.310000 -737.868958 -2.469068
|
||||
1.320000 -736.023071 -2.464319
|
||||
1.330000 -733.925415 -2.457025
|
||||
1.340000 -731.659973 -2.449054
|
||||
1.350000 -729.981873 -2.444898
|
||||
1.360000 -727.800293 -2.441421
|
||||
1.370000 -725.618835 -2.437435
|
||||
1.380000 -723.688965 -2.433025
|
||||
1.390000 -721.759216 -2.430905
|
||||
1.400000 -719.661560 -2.424205
|
||||
1.410000 -717.899536 -2.414367
|
||||
1.420000 -715.718018 -2.416233
|
||||
1.430000 -713.788208 -2.403003
|
||||
1.440000 -711.522766 -2.391639
|
||||
1.450000 -709.760742 -2.383328
|
||||
1.460000 -707.998657 -2.383158
|
||||
1.470000 -705.984985 -2.372218
|
||||
1.480000 -703.635681 -2.377137
|
||||
1.490000 -701.705811 -2.377391
|
||||
1.500000 -699.776001 -2.370183
|
||||
1.510000 -697.846252 -2.365434
|
||||
1.520000 -695.748596 -2.726034
|
||||
1.530000 -693.734802 -2.722727
|
||||
1.540000 -691.888916 -2.729681
|
||||
1.550000 -689.707397 -2.716281
|
||||
1.560000 -687.777588 -2.697030
|
||||
1.570000 -685.763794 -2.677355
|
||||
1.580000 -684.001831 -2.668111
|
||||
1.590000 -681.652466 -2.662683
|
||||
1.600000 -679.890503 -2.646909
|
||||
1.610000 -677.625061 -2.631474
|
||||
1.620000 -675.611267 -2.621382
|
||||
1.630000 -673.849304 -3.134382
|
||||
1.640000 -671.919495 -2.953148
|
||||
1.650000 -669.905762 -2.917614
|
||||
1.660000 -667.975952 -2.634273
|
||||
1.670000 -665.710510 -2.670146
|
||||
1.680000 -663.864624 -2.665312
|
||||
1.690000 -661.431335 -2.793032
|
||||
1.700000 -659.753174 -2.684648
|
||||
1.710000 -657.655640 -2.447781
|
||||
1.720000 -655.809631 -2.459400
|
||||
1.730000 -653.795959 -2.214307
|
||||
1.740000 -651.782227 -2.323878
|
||||
1.750000 -649.852417 -2.461605
|
||||
1.760000 -648.174316 -2.134334
|
||||
1.770000 -645.741089 -2.281559
|
||||
1.780000 -643.727356 -2.102955
|
||||
1.790000 -641.881409 -2.146970
|
||||
1.800000 -639.615967 -2.266039
|
||||
1.810000 -637.518311 -2.363737
|
||||
1.820000 -635.924133 -2.080481
|
||||
1.830000 -633.742554 -2.538271
|
||||
1.840000 -631.812805 -2.472121
|
||||
1.850000 -630.218567 -2.064707
|
||||
1.860000 -627.869263 -2.240512
|
||||
1.870000 -625.687683 -2.340161
|
||||
1.880000 -623.925720 -2.479245
|
||||
1.890000 -621.911987 -2.518935
|
||||
1.900000 -619.898254 -2.519359
|
||||
1.910000 -617.716675 -2.306577
|
||||
1.920000 -615.786865 -2.513168
|
||||
1.930000 -613.689270 -2.541833
|
||||
1.940000 -611.675537 -2.324556
|
||||
1.950000 -609.745728 -2.314549
|
||||
1.960000 -607.815857 -2.303355
|
||||
1.970000 -605.634399 -2.315228
|
||||
1.980000 -603.704529 -2.314719
|
||||
1.990000 -601.858643 -2.302506
|
||||
2.000000 -599.593201 -2.500277
|
||||
2.010000 -597.663391 -2.252131
|
||||
2.020000 -595.817444 -2.413774
|
||||
2.030000 -593.803711 -2.275283
|
||||
2.040000 -591.622192 -2.237290
|
||||
2.050000 -589.608459 -2.236442
|
||||
2.060000 -587.510864 -2.239834
|
||||
2.070000 -585.832764 -2.233049
|
||||
2.080000 -583.567322 -2.228130
|
||||
2.090000 -581.805298 -2.237968
|
||||
2.100000 -579.791565 -2.244583
|
||||
2.110000 -577.777832 -2.200483
|
||||
2.120000 -575.848022 -2.377306
|
||||
2.130000 -573.666504 -2.203367
|
||||
2.140000 -571.820557 -2.181995
|
||||
2.150000 -569.639038 -2.179366
|
||||
2.160000 -567.960938 -2.190815
|
||||
2.170000 -565.779419 -2.180808
|
||||
2.180000 -563.513977 -2.163507
|
||||
2.190000 -561.835815 -2.375780
|
||||
2.200000 -559.654297 -2.135267
|
||||
2.210000 -557.808411 -2.128567
|
||||
2.220000 -555.542969 -2.308697
|
||||
2.230000 -553.613159 -2.307595
|
||||
2.240000 -551.767212 -2.303100
|
||||
2.250000 -549.417847 -2.100495
|
||||
2.260000 -548.075439 -2.131026
|
||||
2.270000 -545.642151 -2.087011
|
||||
2.280000 -543.880127 -2.080057
|
||||
2.290000 -541.614685 -2.073527
|
||||
2.300000 -539.768738 -2.067590
|
||||
2.310000 -537.755066 -2.068438
|
||||
2.320000 -535.657410 -2.062162
|
||||
2.330000 -533.727600 -2.055802
|
||||
2.340000 -531.546082 -2.048509
|
||||
2.350000 -530.035767 -2.043674
|
||||
2.360000 -527.854248 -2.043081
|
||||
2.370000 -525.756592 -2.047661
|
||||
2.380000 -523.658936 -2.039265
|
||||
2.390000 -521.813049 -2.032904
|
||||
2.400000 -519.715393 -2.150956
|
||||
2.410000 -517.953430 -1.847176
|
||||
2.420000 -515.771851 -1.889410
|
||||
2.430000 -513.842102 -1.841409
|
||||
2.440000 -511.492706 -1.839289
|
||||
2.450000 -509.730713 -1.833098
|
||||
2.460000 -507.633087 -1.832844
|
||||
2.470000 -505.787170 -1.824787
|
||||
2.480000 -503.689545 -1.819614
|
||||
2.490000 -501.843597 -1.815373
|
||||
2.500000 -499.829895 -1.807317
|
||||
2.510000 -497.816162 -1.817493
|
||||
2.520000 -495.802460 -1.808674
|
||||
2.530000 -493.872620 -1.803670
|
||||
2.540000 -491.691071 -1.797394
|
||||
2.550000 -489.845184 -1.784334
|
||||
2.560000 -487.915344 -1.786624
|
||||
2.570000 -485.733795 -1.790440
|
||||
2.580000 -483.804016 -1.777719
|
||||
2.590000 -481.706360 -1.768560
|
||||
2.600000 -479.524841 -2.182335
|
||||
2.610000 -477.595032 -2.161981
|
||||
2.620000 -475.749084 -2.157316
|
||||
2.630000 -473.819275 -2.149853
|
||||
2.640000 -471.721649 -2.136030
|
||||
2.650000 -469.875763 -2.134334
|
||||
2.660000 -467.862000 -1.757959
|
||||
2.670000 -465.512665 -1.756602
|
||||
2.680000 -463.582825 -1.761521
|
||||
2.690000 -461.736938 -1.756008
|
||||
2.700000 -459.723206 -1.751344
|
||||
2.710000 -457.793396 -1.748715
|
||||
2.720000 -455.611847 -1.743117
|
||||
2.730000 -453.849854 -1.735146
|
||||
2.740000 -451.668304 -1.734976
|
||||
2.750000 -449.654572 -1.741337
|
||||
2.760000 -447.808685 -1.721067
|
||||
2.770000 -445.627136 -1.710212
|
||||
2.780000 -443.697296 -1.699527
|
||||
2.790000 -442.019196 -1.702495
|
||||
2.800000 -439.585968 -1.699611
|
||||
2.810000 -437.823944 -1.703173
|
||||
2.820000 -435.726318 -1.711060
|
||||
2.830000 -433.544769 -1.708092
|
||||
2.840000 -431.866669 -1.703852
|
||||
2.850000 -429.852936 -1.702749
|
||||
2.860000 -427.671417 -1.695032
|
||||
2.870000 -425.825500 -1.695286
|
||||
2.880000 -423.560059 -2.050544
|
||||
2.890000 -421.546326 -1.687653
|
||||
2.900000 -419.868225 -1.682735
|
||||
2.910000 -417.518890 -2.099732
|
||||
2.920000 -415.672974 -1.665688
|
||||
2.930000 -413.659241 -1.658904
|
||||
2.940000 -411.813354 -1.656784
|
||||
2.950000 -409.463989 -1.658225
|
||||
2.960000 -407.618073 -1.653815
|
||||
2.970000 -405.856079 -1.647285
|
||||
2.980000 -403.758423 -1.653815
|
||||
2.990000 -401.576874 -1.646013
|
||||
3.000000 -399.814880 -1.641349
|
||||
3.010000 -397.801147 -1.643723
|
||||
3.020000 -395.619598 -1.639144
|
||||
3.030000 -393.857605 -1.638720
|
||||
3.040000 -392.011719 -1.634055
|
||||
3.050000 -389.578461 -1.647116
|
||||
3.060000 -387.732544 -1.625490
|
||||
3.070000 -385.634888 -1.627779
|
||||
3.080000 -383.705078 -1.600980
|
||||
3.090000 -381.859161 -1.595637
|
||||
3.100000 -379.845459 -1.591312
|
||||
3.110000 -377.747803 -1.587496
|
||||
3.120000 -375.650177 -1.584782
|
||||
3.130000 -373.804260 -1.583595
|
||||
3.140000 -371.538818 -1.574605
|
||||
3.150000 -369.525085 -1.561799
|
||||
3.160000 -367.846985 -1.559086
|
||||
3.170000 -365.917206 -1.556626
|
||||
3.180000 -363.735657 -1.562902
|
||||
3.190000 -361.889709 -1.554676
|
||||
3.200000 -359.792114 -1.550605
|
||||
3.210000 -357.694489 -1.556541
|
||||
3.220000 -355.596832 -1.542887
|
||||
3.230000 -353.834839 -1.540089
|
||||
3.240000 -351.569397 -1.535000
|
||||
3.250000 -349.807404 -1.529233
|
||||
3.260000 -347.625854 -1.527198
|
||||
3.270000 -345.528229 -1.526265
|
||||
3.280000 -343.682281 -1.525671
|
||||
3.290000 -341.416870 -1.520837
|
||||
3.300000 -339.570953 -1.515410
|
||||
3.310000 -337.389404 -1.511678
|
||||
3.320000 -335.963013 -1.508201
|
||||
3.330000 -333.613678 -1.503197
|
||||
3.340000 -331.599945 -1.501247
|
||||
3.350000 -329.754028 -1.499296
|
||||
3.360000 -327.656433 -1.494971
|
||||
3.370000 -325.558777 -1.491240
|
||||
3.380000 -323.712860 -1.487339
|
||||
3.390000 -321.783051 -1.486660
|
||||
3.400000 -320.021057 -1.483437
|
||||
3.410000 -317.671661 -1.479536
|
||||
3.420000 -315.741852 -1.478603
|
||||
3.430000 -313.812042 -1.473854
|
||||
3.440000 -311.462708 -1.471734
|
||||
3.450000 -309.868500 -1.466306
|
||||
3.460000 -307.770874 -1.463169
|
||||
3.470000 -305.673248 -1.458334
|
||||
3.480000 -303.491699 -1.455366
|
||||
3.490000 -301.729706 -1.451465
|
||||
3.500000 -299.799866 -1.449260
|
||||
3.510000 -297.702240 -1.449175
|
||||
3.520000 -295.604614 -1.445444
|
||||
3.530000 -293.674805 -1.441712
|
||||
3.540000 -291.744995 -1.438829
|
||||
3.550000 -289.563446 -1.436030
|
||||
3.560000 -287.633636 -1.432723
|
||||
3.570000 -285.619873 -1.430602
|
||||
3.580000 -283.690094 -1.427719
|
||||
3.590000 -281.844177 -1.422800
|
||||
3.600000 -279.662628 -1.423055
|
||||
3.610000 -277.816711 -1.418051
|
||||
3.620000 -275.802979 -1.414659
|
||||
3.630000 -273.705353 -1.413387
|
||||
3.640000 -271.523834 -1.409825
|
||||
3.650000 -269.761810 -1.407111
|
||||
3.660000 -267.915894 -1.406348
|
||||
3.670000 -265.650452 -1.401344
|
||||
3.680000 -263.636719 -1.399054
|
||||
3.690000 -261.790802 -1.394899
|
||||
3.700000 -259.273651 -1.391591
|
||||
3.710000 -257.595551 -1.389556
|
||||
3.720000 -255.665741 -1.385231
|
||||
3.730000 -253.652008 -1.383195
|
||||
3.740000 -251.722183 -1.377852
|
||||
3.750000 -249.540665 -1.376156
|
||||
3.760000 -247.778641 -1.371661
|
||||
3.770000 -245.681000 -1.368608
|
||||
3.780000 -243.415573 -1.366064
|
||||
3.790000 -241.821365 -1.364877
|
||||
3.800000 -239.807648 -1.362248
|
||||
3.810000 -237.626129 -1.359449
|
||||
3.820000 -235.612381 -1.355378
|
||||
3.830000 -233.514771 -1.354361
|
||||
3.840000 -231.668854 -1.349357
|
||||
3.850000 -229.655121 -1.345541
|
||||
3.860000 -227.557480 -1.344269
|
||||
3.870000 -225.543762 -1.340961
|
||||
3.880000 -223.613953 -1.337993
|
||||
3.890000 -221.600204 -1.335449
|
||||
3.900000 -219.586487 -1.332904
|
||||
3.910000 -217.572769 -1.329258
|
||||
3.920000 -215.726852 -1.328325
|
||||
3.930000 -213.713135 -1.323576
|
||||
3.940000 -211.867218 -1.321201
|
||||
3.950000 -209.769592 -1.318318
|
||||
3.960000 -207.588058 -1.316876
|
||||
3.970000 -205.658234 -1.314586
|
||||
3.980000 -203.644516 -1.310091
|
||||
3.990000 -201.714691 -1.307717
|
||||
4.000000 -199.700958 -1.304155
|
||||
4.010000 -197.603333 -1.301695
|
||||
4.020000 -195.757416 -1.298473
|
||||
4.030000 -193.491974 -1.295504
|
||||
4.040000 -191.729965 -1.293130
|
||||
4.050000 -189.716232 -1.291518
|
||||
4.060000 -187.702515 -1.289568
|
||||
4.070000 -185.688797 -1.284649
|
||||
4.080000 -183.675064 -1.283801
|
||||
4.090000 -181.745239 -1.278967
|
||||
4.100000 -179.647614 -1.278458
|
||||
4.110000 -177.633896 -1.277356
|
||||
4.120000 -175.620163 -1.272691
|
||||
4.130000 -173.690338 -1.268451
|
||||
4.140000 -171.676620 -1.267094
|
||||
4.150000 -169.662903 -1.263702
|
||||
4.160000 -167.900879 -1.262005
|
||||
4.170000 -165.719345 -1.259461
|
||||
4.180000 -163.537827 -1.256154
|
||||
4.190000 -161.608002 -1.254373
|
||||
4.200000 -159.845993 -1.249115
|
||||
4.210000 -157.748367 -1.248606
|
||||
4.220000 -155.482910 -1.245129
|
||||
4.230000 -153.888718 -1.243348
|
||||
4.240000 -151.707184 -1.239871
|
||||
4.250000 -149.693451 -1.238090
|
||||
4.260000 -147.847549 -1.234274
|
||||
4.270000 -145.749908 -1.232069
|
||||
4.280000 -143.736191 -1.229609
|
||||
4.290000 -141.554657 -1.227998
|
||||
4.300000 -139.708740 -1.224521
|
||||
4.310000 -137.946732 -1.222740
|
||||
4.320000 -135.597382 -1.219178
|
||||
4.330000 -133.919281 -1.217058
|
||||
4.340000 -131.737732 -1.214344
|
||||
4.350000 -129.640106 -1.212648
|
||||
4.360000 -127.458580 -1.209255
|
||||
4.370000 -125.612663 -1.205693
|
||||
4.380000 -123.766747 -1.202810
|
||||
4.390000 -121.669113 -1.200351
|
||||
4.400000 -119.823204 -1.198485
|
||||
4.410000 -117.725563 -1.195516
|
||||
4.420000 -115.711845 -1.192209
|
||||
4.430000 -113.698128 -1.190428
|
||||
4.440000 -111.600487 -1.187460
|
||||
4.450000 -109.586761 -1.185764
|
||||
4.460000 -107.740845 -1.184068
|
||||
4.470000 -105.643219 -1.180082
|
||||
4.480000 -103.629494 -1.178385
|
||||
4.490000 -101.615768 -1.174739
|
||||
4.500000 -99.602043 -1.172534
|
||||
4.510000 -97.504417 -1.170753
|
||||
4.520000 -95.574600 -1.167445
|
||||
4.530000 -93.644768 -1.163459
|
||||
4.540000 -91.966675 -1.161848
|
||||
4.550000 -89.701233 -1.162272
|
||||
4.560000 -87.603600 -1.159473
|
||||
4.570000 -85.925491 -1.154724
|
||||
4.580000 -83.660049 -1.153876
|
||||
4.590000 -81.730232 -1.149551
|
||||
4.600000 -79.716515 -1.149890
|
||||
4.610000 -77.534973 -1.146074
|
||||
4.620000 -75.605156 -1.142003
|
||||
4.630000 -73.591431 -1.140985
|
||||
4.640000 -71.661606 -1.138865
|
||||
4.650000 -69.731789 -1.136999
|
||||
4.660000 -67.718063 -1.134540
|
||||
4.670000 -65.452621 -1.131572
|
||||
4.680000 -63.690613 -1.129621
|
||||
4.690000 -61.676888 -1.126144
|
||||
4.700000 -59.579258 -1.123430
|
||||
4.710000 -57.649437 -1.121225
|
||||
4.720000 -55.635715 -1.119614
|
||||
4.730000 -53.538082 -1.116985
|
||||
4.740000 -51.776077 -1.115543
|
||||
4.750000 -49.594540 -1.115798
|
||||
4.760000 -47.664719 -1.147431
|
||||
4.770000 -45.567089 -1.146667
|
||||
4.780000 -43.469460 -1.141240
|
||||
4.790000 -41.539642 -1.135727
|
||||
4.800000 -39.525917 -1.134540
|
||||
4.810000 -37.680000 -1.132081
|
||||
4.820000 -35.834084 -1.127671
|
||||
4.830000 -33.904266 -1.125635
|
||||
4.840000 -31.806633 -1.123345
|
||||
4.850000 -29.709005 -1.122582
|
||||
4.860000 -27.611376 -1.120547
|
||||
4.870000 -25.513744 -1.115628
|
||||
4.880000 -23.583923 -1.113678
|
||||
4.890000 -21.486296 -1.112914
|
||||
4.900000 -19.808189 -1.109352
|
||||
4.910000 -17.458843 -1.107487
|
||||
4.920000 -15.529024 -1.105536
|
||||
4.930000 -13.431395 -1.103077
|
||||
4.940000 -11.501575 -1.099091
|
||||
4.950000 -9.403944 -1.095020
|
||||
4.960000 -7.474124 -1.092645
|
||||
4.970000 -5.879925 -1.090016
|
||||
4.980000 -3.698390 -1.088914
|
||||
4.990000 -1.684664 -1.086709
|
||||
5.000000 0.412966 -1.084080
|
||||
5.010000 2.510596 -1.081620
|
||||
5.020000 4.524321 -1.077804
|
||||
5.030000 6.538046 -1.077550
|
||||
5.040000 8.635676 -1.074581
|
||||
5.050000 10.145969 -1.070765
|
||||
5.060000 12.327506 -1.068984
|
||||
5.070000 14.341230 -1.064404
|
||||
5.080000 16.438860 -1.065168
|
||||
5.090000 18.536491 -1.062454
|
||||
5.100000 20.466312 -1.057959
|
||||
5.110000 22.563942 -1.056857
|
||||
5.120000 24.493761 -1.054228
|
||||
5.130000 26.423580 -1.052871
|
||||
5.140000 28.353401 -1.052023
|
||||
5.150000 30.451031 -1.048376
|
||||
5.160000 32.464756 -1.046510
|
||||
5.170000 34.562386 -1.044644
|
||||
5.180000 36.408302 -1.042185
|
||||
5.190000 38.422028 -1.042354
|
||||
5.200000 40.435749 -1.040658
|
||||
5.210000 42.197762 -1.037181
|
||||
5.220000 44.295391 -1.035061
|
||||
5.230000 46.393021 -1.032178
|
||||
5.240000 48.155029 -1.032008
|
||||
5.250000 50.420471 -1.028361
|
||||
5.260000 52.685913 -1.025139
|
||||
5.270000 54.447922 -1.024291
|
||||
5.280000 56.293835 -1.022425
|
||||
5.290000 58.307560 -1.022001
|
||||
5.300000 60.321289 -1.017082
|
||||
5.310000 62.502819 -1.015640
|
||||
5.320000 64.516548 -1.013944
|
||||
5.330000 66.698082 -1.009873
|
||||
5.340000 68.376183 -1.006651
|
||||
5.350000 70.389908 -1.005463
|
||||
5.360000 72.571442 -1.003174
|
||||
5.370000 74.669075 -1.001562
|
||||
5.380000 76.514992 -0.999696
|
||||
5.390000 78.528709 -0.997831
|
||||
5.400000 80.626350 -0.995287
|
||||
5.410000 82.640068 -0.993675
|
||||
5.420000 84.485985 -0.992149
|
||||
5.430000 86.499710 -0.990028
|
||||
5.440000 88.513435 -0.987060
|
||||
5.450000 90.443260 -0.984686
|
||||
5.460000 92.205261 -0.982141
|
||||
5.470000 94.302895 -0.980869
|
||||
5.480000 96.316620 -0.977731
|
||||
5.490000 98.330345 -0.975102
|
||||
5.500000 100.427979 -0.975017
|
||||
5.510000 102.525604 -0.972558
|
||||
5.520000 104.455421 -0.970692
|
||||
5.530000 106.636963 -0.967300
|
||||
5.540000 108.231163 -0.966961
|
||||
5.550000 110.412689 -0.965180
|
||||
5.560000 112.510323 -0.963484
|
||||
5.570000 114.524055 -0.961024
|
||||
5.580000 116.705582 -0.958226
|
||||
5.590000 118.383690 -0.956869
|
||||
5.600000 120.397415 -0.954070
|
||||
5.610000 122.327240 -0.952374
|
||||
5.620000 124.508766 -0.950169
|
||||
5.630000 126.354691 -0.946861
|
||||
5.640000 128.620132 -0.946268
|
||||
5.650000 130.466049 -0.942706
|
||||
5.660000 132.395859 -0.941858
|
||||
5.670000 134.241776 -0.939568
|
||||
5.680000 136.507217 -0.937363
|
||||
5.690000 138.520935 -0.936006
|
||||
5.700000 140.534668 -0.932953
|
||||
5.710000 142.464493 -0.931172
|
||||
5.720000 144.646027 -0.930409
|
||||
5.730000 145.988510 -0.926932
|
||||
5.740000 148.421753 -0.926847
|
||||
5.750000 150.603287 -0.923200
|
||||
5.760000 152.617020 -0.922098
|
||||
5.770000 154.546844 -0.919469
|
||||
5.780000 156.392761 -0.917857
|
||||
5.790000 158.406479 -0.914974
|
||||
5.800000 160.336304 -0.912345
|
||||
5.810000 162.517838 -0.911412
|
||||
5.820000 164.363739 -0.910055
|
||||
5.830000 166.461380 -0.906663
|
||||
5.840000 168.475098 -0.903949
|
||||
5.850000 170.404922 -0.901235
|
||||
5.860000 172.502563 -0.899624
|
||||
5.870000 174.516281 -0.899115
|
||||
5.880000 176.613922 -0.899285
|
||||
5.890000 178.459824 -0.894705
|
||||
5.900000 180.725266 -0.892585
|
||||
5.910000 182.235550 -0.890889
|
||||
5.920000 184.500992 -0.888684
|
||||
5.930000 186.598633 -0.886055
|
||||
5.940000 188.612350 -0.885461
|
||||
5.950000 190.542175 -0.883426
|
||||
5.960000 192.304169 -0.880118
|
||||
5.970000 194.317917 -0.879694
|
||||
5.980000 196.499435 -0.876641
|
||||
5.990000 198.429276 -0.874182
|
||||
6.000000 200.442993 -0.874351
|
||||
6.010000 202.624512 -0.869517
|
||||
6.020000 204.470428 -0.869432
|
||||
6.030000 206.484146 -0.866803
|
||||
6.040000 208.413986 -0.864938
|
||||
6.050000 210.679428 -0.864005
|
||||
6.060000 212.525330 -0.863326
|
||||
6.070000 214.371246 -0.860188
|
||||
6.080000 216.384964 -0.860188
|
||||
6.090000 218.398712 -0.856796
|
||||
6.100000 220.412430 -0.854676
|
||||
6.110000 222.426147 -0.852217
|
||||
6.120000 224.355972 -0.850520
|
||||
6.130000 226.537506 -0.849672
|
||||
6.140000 228.299515 -0.848061
|
||||
6.150000 230.397141 -0.845347
|
||||
6.160000 232.578690 -0.843397
|
||||
6.170000 234.676300 -0.841785
|
||||
6.180000 236.270523 -0.840768
|
||||
6.190000 238.368134 -0.839750
|
||||
6.200000 240.381866 -0.836866
|
||||
6.210000 242.311676 -0.835764
|
||||
6.220000 244.577118 -0.833389
|
||||
6.230000 246.506943 -0.831693
|
||||
6.240000 248.520676 -0.830167
|
||||
6.250000 250.198776 -0.827538
|
||||
6.260000 252.380310 -0.825587
|
||||
6.270000 254.561844 -0.823212
|
||||
6.280000 256.407776 -0.821347
|
||||
6.290000 258.673218 -0.819905
|
||||
6.300000 260.351318 -0.816173
|
||||
6.310000 262.365021 -0.814986
|
||||
6.320000 264.462677 -0.814986
|
||||
6.330000 266.224670 -0.813205
|
||||
6.340000 268.574036 -0.810661
|
||||
6.350000 270.503845 -0.807269
|
||||
6.360000 272.433655 -0.805827
|
||||
6.370000 274.615173 -0.804809
|
||||
6.380000 276.712830 -0.804724
|
||||
6.390000 278.558746 -0.801841
|
||||
6.400000 280.488556 -0.799806
|
||||
6.410000 282.334473 -0.798364
|
||||
6.420000 284.683807 -0.796837
|
||||
6.430000 286.529724 -0.794802
|
||||
6.440000 288.543457 -0.792173
|
||||
6.450000 290.305450 -0.791749
|
||||
6.460000 292.487000 -0.787848
|
||||
6.470000 294.416809 -0.786321
|
||||
6.480000 296.430542 -0.784795
|
||||
6.490000 298.695984 -0.783183
|
||||
6.500000 300.290192 -0.781233
|
||||
6.510000 302.387817 -0.779452
|
||||
6.520000 304.401550 -0.778858
|
||||
6.530000 306.499176 -0.775975
|
||||
6.540000 308.680725 -0.774618
|
||||
6.550000 310.526611 -0.773346
|
||||
6.560000 312.288635 -0.771056
|
||||
6.570000 314.470184 -0.769275
|
||||
6.580000 316.735626 -0.767409
|
||||
6.590000 318.413727 -0.766561
|
||||
6.600000 320.343536 -0.765119
|
||||
6.610000 322.441162 -0.764695
|
||||
6.620000 324.538788 -0.762575
|
||||
6.630000 326.384705 -0.760540
|
||||
6.640000 328.482330 -0.759946
|
||||
6.650000 330.663879 -0.759353
|
||||
6.660000 332.425873 -0.756215
|
||||
6.670000 334.439606 -0.753586
|
||||
6.680000 336.621155 -0.752398
|
||||
6.690000 338.550964 -0.750448
|
||||
6.700000 340.564697 -0.748667
|
||||
6.710000 342.662292 -0.746886
|
||||
6.720000 344.424347 -0.744681
|
||||
6.730000 346.438049 -0.742900
|
||||
6.740000 348.535675 -0.740356
|
||||
6.750000 350.549408 -0.739084
|
||||
6.760000 352.647064 -0.739084
|
||||
6.770000 354.576843 -0.735691
|
||||
6.780000 356.674500 -0.734080
|
||||
6.790000 358.436493 -0.731451
|
||||
6.800000 360.534119 -0.729924
|
||||
6.810000 362.547882 -0.729331
|
||||
6.820000 364.477661 -0.727889
|
||||
6.830000 366.407471 -0.725854
|
||||
6.840000 368.589020 -0.724666
|
||||
6.850000 370.518860 -0.722292
|
||||
6.860000 372.532593 -0.734334
|
||||
6.870000 374.462402 -0.732045
|
||||
6.880000 376.560028 -0.729585
|
||||
6.890000 378.573730 -0.728737
|
||||
6.900000 380.419678 -0.728059
|
||||
6.910000 382.517303 -0.726447
|
||||
6.920000 384.531006 -0.724158
|
||||
6.930000 386.544739 -0.726278
|
||||
6.940000 388.558441 -0.723564
|
||||
6.950000 390.739990 -0.720596
|
||||
6.960000 392.585938 -0.717712
|
||||
6.970000 394.599640 -0.714659
|
||||
6.980000 396.361664 -0.715507
|
||||
6.990000 398.375366 -0.701344
|
||||
7.000000 400.305206 -0.699479
|
||||
7.010000 402.570648 -0.696680
|
||||
7.020000 404.668274 -0.698631
|
||||
7.030000 406.262482 -0.695832
|
||||
7.040000 408.444031 -0.695153
|
||||
7.050000 410.457733 -0.692864
|
||||
7.060000 412.555359 -0.691337
|
||||
7.070000 414.485168 -0.689471
|
||||
7.080000 416.582794 -0.688199
|
||||
7.090000 418.344788 -0.685231
|
||||
7.100000 420.442444 -0.685231
|
||||
7.110000 422.372284 -0.683365
|
||||
7.120000 424.637726 -0.681584
|
||||
7.130000 426.567505 -0.679464
|
||||
7.140000 428.581268 -0.678701
|
||||
7.150000 430.678894 -0.677259
|
||||
7.160000 432.608704 -0.674206
|
||||
7.170000 434.454590 -0.672764
|
||||
7.180000 436.720032 -0.671831
|
||||
7.190000 438.649872 -0.669796
|
||||
7.200000 440.579712 -0.670135
|
||||
7.210000 442.677307 -0.666913
|
||||
7.220000 444.607147 -0.665895
|
||||
7.230000 446.788696 -0.663775
|
||||
7.240000 448.550690 -0.662079
|
||||
7.250000 450.648315 -0.659534
|
||||
7.260000 452.745941 -0.658347
|
||||
7.270000 454.591858 -0.655972
|
||||
7.280000 456.437775 -0.653937
|
||||
7.290000 458.535400 -0.653598
|
||||
7.300000 460.465240 -0.651986
|
||||
7.310000 462.562866 -0.650460
|
||||
7.320000 464.492676 -0.648933
|
||||
7.330000 466.506378 -0.647237
|
||||
7.340000 468.687927 -0.644015
|
||||
7.350000 470.533875 -0.642234
|
||||
7.360000 472.379761 -0.642149
|
||||
7.370000 474.645203 -0.640113
|
||||
7.380000 476.407196 -0.637654
|
||||
7.390000 478.588745 -0.636297
|
||||
7.400000 480.518585 -0.634177
|
||||
7.410000 482.364471 -0.632226
|
||||
7.420000 484.462128 -0.630869
|
||||
7.430000 486.475861 -0.629173
|
||||
7.440000 488.573456 -0.627477
|
||||
7.450000 490.671112 -0.625272
|
||||
7.460000 492.768738 -0.625272
|
||||
7.470000 494.530731 -0.624509
|
||||
7.480000 496.628387 -0.622134
|
||||
7.490000 498.642120 -0.620523
|
||||
7.500000 500.571930 -0.618742
|
||||
7.510000 502.501709 -0.616113
|
||||
7.520000 504.767151 -0.614841
|
||||
7.530000 506.780914 -0.613654
|
||||
7.540000 508.626831 -0.612466
|
||||
7.550000 510.472748 -0.610940
|
||||
7.560000 512.318665 -0.609752
|
||||
7.570000 514.500183 -0.608226
|
||||
7.580000 516.346069 -0.607039
|
||||
7.590000 518.359802 -0.605427
|
||||
7.600000 520.373596 -0.602629
|
||||
7.610000 522.471191 -0.601441
|
||||
7.620000 524.568787 -0.600254
|
||||
7.630000 526.750366 -0.598812
|
||||
7.640000 528.764038 -0.597286
|
||||
7.650000 530.609985 -0.595844
|
||||
7.660000 532.623718 -0.593639
|
||||
7.670000 534.553528 -0.593215
|
||||
7.680000 536.567261 -0.591688
|
||||
7.690000 538.664856 -0.590332
|
||||
7.700000 540.510803 -0.588805
|
||||
7.710000 542.440613 -0.586685
|
||||
7.720000 544.538269 -0.585667
|
||||
7.730000 546.468079 -0.584904
|
||||
7.740000 548.565674 -0.583971
|
||||
7.750000 550.327698 -0.581427
|
||||
7.760000 552.677063 -0.581088
|
||||
7.770000 554.522949 -0.578374
|
||||
7.780000 556.620605 -0.577017
|
||||
7.790000 558.550415 -0.575066
|
||||
7.800000 560.648071 -0.574727
|
||||
7.810000 562.577881 -0.572776
|
||||
7.820000 564.507690 -0.570656
|
||||
7.830000 566.353638 -0.568790
|
||||
7.840000 568.283447 -0.569384
|
||||
7.850000 570.464966 -0.566077
|
||||
7.860000 572.478699 -0.564720
|
||||
7.870000 574.492432 -0.563108
|
||||
7.880000 576.673950 -0.562006
|
||||
7.890000 578.435974 -0.560055
|
||||
7.900000 580.617493 -0.558359
|
||||
7.910000 582.463440 -0.557172
|
||||
7.920000 584.728882 -0.555730
|
||||
7.930000 586.574768 -0.553186
|
||||
7.940000 588.672424 -0.552253
|
||||
7.950000 590.434387 -0.550811
|
||||
7.960000 592.364258 -0.548776
|
||||
7.970000 594.461853 -0.546401
|
||||
7.980000 596.727295 -0.546147
|
||||
7.990000 598.741028 -0.544536
|
||||
8.000000 600.502991 -0.542755
|
||||
8.010000 602.432861 -0.543263
|
||||
8.020000 604.446533 -0.540295
|
||||
8.030000 606.628113 -0.540465
|
||||
8.040000 608.641846 -0.538514
|
||||
8.050000 610.487732 -0.536818
|
||||
8.060000 612.585388 -0.535546
|
||||
8.070000 614.682983 -0.533171
|
||||
8.080000 616.361084 -0.531136
|
||||
8.090000 618.542664 -0.529864
|
||||
8.100000 620.388550 -0.529610
|
||||
8.110000 622.570068 -0.528592
|
||||
8.120000 624.583801 -0.526387
|
||||
8.130000 626.597595 -0.526013
|
||||
8.140000 628.527344 -0.524972
|
||||
8.150000 630.625000 -0.523219
|
||||
8.160000 632.722656 -0.521918
|
||||
8.170000 634.484680 -0.519963
|
||||
8.180000 636.414429 -0.519291
|
||||
8.190000 638.679871 -0.517773
|
||||
8.200000 640.525757 -0.516850
|
||||
8.210000 642.455627 -0.514760
|
||||
8.220000 644.553223 -0.513535
|
||||
8.230000 646.399170 -0.512100
|
||||
8.240000 648.580750 -0.511270
|
||||
8.250000 650.510559 -0.509365
|
||||
8.260000 652.356445 -0.508333
|
||||
8.270000 654.537964 -0.507527
|
||||
8.280000 656.803406 -0.505253
|
||||
8.290000 658.733215 -0.504330
|
||||
8.300000 660.579163 -0.502526
|
||||
8.310000 662.592834 -0.501083
|
||||
8.320000 664.606628 -0.499715
|
||||
8.330000 666.620300 -0.498524
|
||||
8.340000 668.466248 -0.497433
|
||||
8.350000 670.563843 -0.496761
|
||||
8.360000 672.409851 -0.494529
|
||||
8.370000 674.423523 -0.492885
|
||||
8.380000 676.521179 -0.492138
|
||||
8.390000 678.702698 -0.495855
|
||||
8.400000 680.548584 -0.494278
|
||||
8.410000 682.562378 -0.493279
|
||||
8.420000 684.995605 -0.492364
|
||||
8.430000 686.589783 -0.489847
|
||||
8.440000 688.435669 -0.489058
|
||||
8.450000 690.533325 -0.487422
|
||||
8.460000 692.463135 -0.485920
|
||||
8.470000 694.476868 -0.484552
|
||||
8.480000 696.490601 -0.483747
|
||||
8.490000 698.336487 -0.481993
|
||||
8.500000 700.518005 -0.480189
|
||||
8.510000 702.363892 -0.479224
|
||||
8.520000 704.629333 -0.477932
|
||||
8.530000 706.643127 -0.477109
|
||||
8.540000 708.740784 -0.475498
|
||||
8.550000 710.670532 -0.474760
|
||||
8.560000 712.768188 -0.473249
|
||||
8.570000 714.614136 -0.471731
|
||||
8.580000 716.627869 -0.469322
|
||||
8.590000 718.725464 -0.467460
|
||||
8.600000 720.823120 -0.466318
|
||||
8.610000 722.417297 -0.464917
|
||||
8.620000 724.682739 -0.463969
|
||||
8.630000 726.528625 -0.463516
|
||||
8.640000 728.542419 -0.461443
|
||||
8.650000 730.472168 -0.460713
|
||||
8.660000 732.569824 -0.458523
|
||||
8.670000 734.667419 -0.457449
|
||||
8.680000 736.597290 -0.455796
|
||||
8.690000 738.443176 -0.454688
|
||||
8.700000 740.624756 -0.453447
|
||||
8.710000 742.470642 -0.451945
|
||||
8.720000 744.568298 -0.451013
|
||||
8.730000 746.414185 -0.449645
|
||||
8.740000 748.595703 -0.448353
|
||||
8.750000 750.609497 -0.447036
|
||||
8.760000 752.707031 -0.446490
|
||||
8.770000 754.804688 -0.444829
|
||||
8.780000 756.734497 -0.443377
|
||||
8.790000 758.580444 -0.442093
|
||||
8.800000 760.510254 -0.440885
|
||||
8.810000 762.859619 -0.439828
|
||||
8.820000 764.621582 -0.438158
|
||||
8.830000 766.551453 -0.437436
|
||||
8.840000 768.481262 -0.435917
|
||||
8.850000 770.662781 -0.435145
|
||||
8.860000 772.676575 -0.433669
|
||||
8.870000 774.522461 -0.433845
|
||||
8.880000 776.703979 -0.432024
|
||||
8.890000 778.382080 -0.430631
|
||||
8.900000 780.731445 -0.428542
|
||||
8.910000 782.577332 -0.427124
|
||||
8.920000 784.423218 -0.425924
|
||||
8.930000 786.604797 -0.424363
|
||||
8.940000 788.534607 -0.423171
|
||||
8.950000 790.380554 -0.421418
|
||||
8.960000 792.645996 -0.420973
|
||||
8.970000 794.407959 -0.420042
|
||||
8.980000 796.505615 -0.418514
|
||||
8.990000 798.435425 -0.417541
|
||||
9.000000 800.616943 -0.416870
|
||||
9.010000 802.798523 -0.415947
|
||||
9.020000 804.644409 -0.416182
|
||||
9.030000 806.574219 -0.413958
|
||||
9.040000 808.587952 -0.415066
|
||||
9.050000 810.685608 -0.416735
|
||||
9.060000 812.699280 -0.413194
|
||||
9.070000 814.964722 -0.410753
|
||||
9.080000 816.642822 -0.411155
|
||||
9.090000 818.740479 -0.408739
|
||||
9.100000 820.670288 -0.408059
|
||||
9.110000 822.516174 -0.408218
|
||||
9.120000 824.446045 -0.407153
|
||||
9.130000 826.627563 -0.403654
|
||||
9.140000 828.557373 -0.404342
|
||||
9.150000 830.571106 -0.402345
|
||||
9.160000 832.584839 -0.401296
|
||||
9.170000 834.514709 -0.399777
|
||||
9.180000 836.528381 -0.398879
|
||||
9.190000 838.458252 -0.398309
|
||||
9.200000 840.639709 -0.398300
|
||||
9.210000 842.569580 -0.396538
|
||||
9.220000 844.583252 -0.397914
|
||||
9.230000 846.596985 -0.393895
|
||||
9.240000 848.526794 -0.392938
|
||||
9.250000 850.624451 -0.392552
|
||||
9.260000 852.638184 -0.424145
|
||||
9.270000 854.567993 -0.424052
|
||||
9.280000 856.581726 -0.420788
|
||||
9.290000 858.511536 -0.420042
|
||||
9.300000 860.860901 -0.421015
|
||||
9.310000 862.622864 -0.420092
|
||||
9.320000 864.636658 -0.418338
|
||||
9.330000 866.482544 -0.417247
|
||||
9.340000 868.412354 -0.416299
|
||||
9.350000 870.258301 -0.413538
|
||||
9.360000 872.439819 -0.413211
|
||||
9.370000 874.537415 -0.412473
|
||||
9.380000 876.299438 -0.410610
|
||||
9.390000 878.564880 -0.410249
|
||||
9.400000 880.578613 -0.407363
|
||||
9.410000 882.844055 -0.406339
|
||||
9.420000 884.438293 -0.405542
|
||||
9.430000 886.535828 -0.404040
|
||||
9.440000 888.633484 -0.402722
|
||||
9.450000 890.647278 -0.402638
|
||||
9.460000 892.744812 -0.401296
|
||||
9.470000 894.758606 -0.398409
|
||||
9.480000 896.604492 -0.397897
|
||||
9.490000 898.450378 -0.396622
|
||||
9.500000 900.715820 -0.396286
|
||||
9.510000 902.310059 -0.394558
|
||||
9.520000 904.659363 -0.392762
|
||||
9.530000 906.673157 -0.390757
|
||||
9.540000 908.519043 -0.390027
|
||||
9.550000 910.784485 -0.387694
|
||||
9.560000 912.630432 -0.387434
|
||||
9.570000 914.560242 -0.385839
|
||||
9.580000 916.406128 -0.384572
|
||||
9.590000 918.755493 -0.382970
|
||||
9.600000 920.937012 -0.382651
|
||||
9.610000 922.531250 -0.381283
|
||||
9.620000 924.544983 -0.381224
|
||||
9.630000 926.642578 -0.378833
|
||||
9.640000 928.656311 -0.376651
|
||||
9.650000 930.502197 -0.376148
|
||||
9.660000 932.767639 -0.374620
|
||||
9.670000 934.529663 -0.374931
|
||||
9.680000 936.375549 -0.373823
|
||||
9.690000 938.724976 -0.372539
|
||||
9.700000 940.654724 -0.370668
|
||||
9.710000 942.584534 -0.369754
|
||||
9.720000 944.598267 -0.367992
|
||||
9.730000 946.528076 -0.366305
|
||||
9.740000 948.877441 -0.364803
|
||||
9.750000 950.471619 -0.363611
|
||||
9.760000 952.485413 -0.362269
|
||||
9.770000 954.666931 -0.361220
|
||||
9.780000 956.764526 -0.359802
|
||||
9.790000 958.610474 -0.358510
|
||||
9.800000 960.624146 -0.356764
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,199 +0,0 @@
|
||||
Time(s) E(mV) I(mA)
|
||||
0.100000 1.763577 -0.029101
|
||||
0.200000 2.762724 -0.028610
|
||||
0.300000 3.770268 -0.028559
|
||||
0.400000 4.786208 -0.028826
|
||||
0.500000 5.768563 -0.028349
|
||||
0.600000 6.792899 -0.028234
|
||||
0.700000 7.775254 -0.028022
|
||||
0.800000 8.782798 -0.028118
|
||||
0.900000 9.765153 -0.028299
|
||||
1.000000 10.789489 -0.028453
|
||||
1.100000 11.780240 -0.028453
|
||||
1.200000 12.821369 -0.028657
|
||||
1.300000 13.770139 -0.028915
|
||||
1.400000 14.777682 -0.028546
|
||||
1.500000 15.776831 -0.029188
|
||||
1.600000 16.767582 -0.028683
|
||||
1.700000 17.783520 -0.028601
|
||||
1.800000 18.782669 -0.028970
|
||||
1.900000 19.807005 -0.028870
|
||||
2.000000 20.797756 -0.028791
|
||||
2.100000 21.805300 -0.028462
|
||||
2.200000 22.737278 -0.028364
|
||||
2.300000 23.778406 -0.028617
|
||||
2.400000 24.752365 -0.028738
|
||||
2.500000 25.793493 -0.029199
|
||||
2.600000 26.767452 -0.028708
|
||||
2.700000 27.783392 -0.028065
|
||||
2.800000 28.799332 -0.027991
|
||||
2.900000 29.806875 -0.028424
|
||||
3.000000 30.780836 -0.028417
|
||||
3.100000 31.796772 -0.028475
|
||||
3.200000 32.787525 -0.028331
|
||||
3.300000 33.778278 -0.028108
|
||||
3.400000 34.769028 -0.028230
|
||||
3.500000 35.784969 -0.027984
|
||||
3.600000 36.784115 -0.027942
|
||||
3.700000 37.766468 -0.028032
|
||||
3.800000 38.790806 -0.028286
|
||||
3.900000 39.789955 -0.028354
|
||||
4.000000 40.763912 -0.028207
|
||||
4.100000 41.754665 -0.028099
|
||||
4.200000 42.770603 -0.028197
|
||||
4.300000 43.786545 -0.028176
|
||||
4.400000 44.777294 -0.028192
|
||||
4.500000 45.784836 -0.028307
|
||||
4.600000 46.775589 -0.028396
|
||||
4.700000 47.774738 -0.028483
|
||||
4.800000 48.773884 -0.028376
|
||||
4.900000 49.781429 -0.028730
|
||||
5.000000 50.780575 -0.029071
|
||||
5.100000 51.788116 -0.029210
|
||||
5.200000 52.770473 -0.028525
|
||||
5.300000 53.752831 -0.028479
|
||||
5.400000 54.768772 -0.029232
|
||||
5.500000 55.793106 -0.029103
|
||||
5.600000 56.800652 -0.029043
|
||||
5.700000 57.766212 -0.029321
|
||||
5.800000 58.765362 -0.029204
|
||||
5.900000 59.789696 -0.029369
|
||||
6.000000 60.797237 -0.029143
|
||||
6.100000 61.787991 -0.028959
|
||||
6.200000 62.803932 -0.028818
|
||||
6.300000 63.769493 -0.029102
|
||||
6.400000 64.768639 -0.029005
|
||||
6.500000 65.759392 -0.028507
|
||||
6.600000 66.783730 -0.029151
|
||||
6.700000 67.774475 -0.028881
|
||||
6.800000 68.790421 -0.028720
|
||||
6.900000 69.789566 -0.028786
|
||||
7.000000 70.797112 -0.028745
|
||||
7.100000 71.813049 -0.028627
|
||||
7.200000 72.795403 -0.028186
|
||||
7.300000 73.802956 -0.028823
|
||||
7.400000 74.793701 -0.028599
|
||||
7.500000 75.776054 -0.028958
|
||||
7.600000 76.800392 -0.029110
|
||||
7.700000 77.791145 -0.029458
|
||||
7.800000 78.781891 -0.029195
|
||||
7.900000 79.797829 -0.029496
|
||||
8.000000 80.796974 -0.029733
|
||||
8.100000 81.796127 -0.029937
|
||||
8.200000 82.795273 -0.029791
|
||||
8.300000 83.777634 -0.029210
|
||||
8.400000 84.801971 -0.029492
|
||||
8.500000 85.792717 -0.029031
|
||||
8.600000 86.775070 -0.029057
|
||||
8.700000 87.816200 -0.029236
|
||||
8.800000 88.790161 -0.028967
|
||||
8.900000 89.772514 -0.029038
|
||||
9.000000 90.796852 -0.028881
|
||||
9.100000 91.795998 -0.029188
|
||||
9.200000 92.803543 -0.029207
|
||||
9.300000 93.760704 -0.029310
|
||||
9.400000 94.810234 -0.029037
|
||||
9.500000 95.784195 -0.029098
|
||||
9.600000 96.800133 -0.029167
|
||||
9.700000 97.807678 -0.029221
|
||||
9.800000 98.790031 -0.029132
|
||||
9.900000 99.797577 -0.028941
|
||||
10.000000 100.796722 -0.029081
|
||||
10.100000 101.804268 -0.029024
|
||||
10.200000 102.803413 -0.028922
|
||||
10.300000 103.802567 -0.028957
|
||||
10.400000 104.801712 -0.028903
|
||||
10.500000 105.800858 -0.029129
|
||||
10.600000 106.783211 -0.029139
|
||||
10.700000 107.807549 -0.029148
|
||||
10.800000 108.815094 -0.029216
|
||||
10.900000 109.805840 -0.028921
|
||||
11.000000 110.813385 -0.028742
|
||||
11.100000 111.804138 -0.028889
|
||||
11.200000 112.769699 -0.028884
|
||||
11.300000 113.802429 -0.028933
|
||||
11.400000 114.809982 -0.028740
|
||||
11.500000 115.809128 -0.028485
|
||||
11.600000 116.816666 -0.029093
|
||||
11.700000 117.807419 -0.028899
|
||||
11.800000 118.814964 -0.029117
|
||||
11.900000 119.797318 -0.029162
|
||||
12.000000 120.788063 -0.029299
|
||||
12.100000 121.787216 -0.029617
|
||||
12.200000 122.794762 -0.029564
|
||||
12.300000 123.785507 -0.029263
|
||||
12.400000 124.809845 -0.028942
|
||||
12.500000 125.800598 -0.029249
|
||||
12.600000 126.799744 -0.029054
|
||||
12.700000 127.815689 -0.029312
|
||||
12.800000 128.823242 -0.029264
|
||||
12.900000 129.813980 -0.029597
|
||||
13.000000 130.813126 -0.029762
|
||||
13.100000 131.803879 -0.029987
|
||||
13.200000 132.803024 -0.029859
|
||||
13.300000 133.793777 -0.029861
|
||||
13.400000 134.792923 -0.029838
|
||||
13.500000 135.808868 -0.029612
|
||||
13.600000 136.791214 -0.029442
|
||||
13.700000 137.790359 -0.029729
|
||||
13.800000 138.806320 -0.029585
|
||||
13.900000 139.805466 -0.029730
|
||||
14.000000 140.829788 -0.029783
|
||||
14.100000 141.812134 -0.029521
|
||||
14.200000 142.802902 -0.029773
|
||||
14.300000 143.802048 -0.029522
|
||||
14.400000 144.801193 -0.029461
|
||||
14.500000 145.775146 -0.029776
|
||||
14.600000 146.816284 -0.029446
|
||||
14.700000 147.807022 -0.029666
|
||||
14.800000 148.822968 -0.029733
|
||||
14.900000 149.805328 -0.029659
|
||||
15.000000 150.796066 -0.029416
|
||||
15.100000 151.812012 -0.029498
|
||||
15.200000 152.819565 -0.029405
|
||||
15.300000 153.801910 -0.029533
|
||||
15.400000 154.809464 -0.029788
|
||||
15.500000 155.817001 -0.029578
|
||||
15.600000 156.790955 -0.029868
|
||||
15.700000 157.815308 -0.029453
|
||||
15.800000 158.814453 -0.029587
|
||||
15.900000 159.821991 -0.029697
|
||||
16.000000 160.795944 -0.029583
|
||||
16.100000 161.795090 -0.029541
|
||||
16.200001 162.794235 -0.029603
|
||||
16.299999 163.826965 -0.029526
|
||||
16.400000 164.800934 -0.029500
|
||||
16.500000 165.833664 -0.029578
|
||||
16.600000 166.824417 -0.029656
|
||||
16.700001 167.798370 -0.029669
|
||||
16.799999 168.814316 -0.029662
|
||||
16.900000 169.805069 -0.029525
|
||||
17.000000 170.812607 -0.029632
|
||||
17.100000 171.803375 -0.029763
|
||||
17.200001 172.827698 -0.029450
|
||||
17.299999 173.818451 -0.029243
|
||||
17.400000 174.809189 -0.029499
|
||||
17.500000 175.799957 -0.029474
|
||||
17.600000 176.799103 -0.029973
|
||||
17.700001 177.823441 -0.029679
|
||||
17.799999 178.805786 -0.029621
|
||||
17.900000 179.813339 -0.029742
|
||||
18.000000 180.829285 -0.029791
|
||||
18.100000 181.786438 -0.029615
|
||||
18.200001 182.810776 -0.029776
|
||||
18.299999 183.826721 -0.029699
|
||||
18.400000 184.809067 -0.029858
|
||||
18.500000 185.799820 -0.029694
|
||||
18.600000 186.782181 -0.029660
|
||||
18.700001 187.814911 -0.029505
|
||||
18.799999 188.822464 -0.029578
|
||||
18.900000 189.813202 -0.029569
|
||||
19.000000 190.812363 -0.029655
|
||||
19.100000 191.811508 -0.029725
|
||||
19.200001 192.810654 -0.029956
|
||||
19.299999 193.809799 -0.029307
|
||||
19.400000 194.808945 -0.029470
|
||||
19.500000 195.799698 -0.029415
|
||||
19.600000 196.807236 -0.029424
|
||||
19.700001 197.856766 -0.029606
|
||||
19.799999 198.813934 -0.029029
|
||||
@@ -1,89 +0,0 @@
|
||||
Time(s) E(mV) I(mA)
|
||||
0.100000 1100.313599 0.099854
|
||||
0.200000 1100.313599 0.099846
|
||||
0.300000 1100.313599 0.099829
|
||||
0.400000 1100.313599 0.099829
|
||||
0.500000 1100.313599 0.099821
|
||||
0.600000 1100.313599 0.099829
|
||||
0.700000 1100.313599 0.099846
|
||||
0.800000 1100.313599 0.099812
|
||||
0.900000 1100.313599 0.099812
|
||||
1.000000 1100.313599 0.099829
|
||||
1.100000 1100.313599 0.099812
|
||||
1.200000 1100.313599 0.099804
|
||||
1.300000 1100.313599 0.099796
|
||||
1.400000 1100.313599 0.099804
|
||||
1.500000 1100.313599 0.099804
|
||||
1.600000 1100.313599 0.099787
|
||||
1.700000 1100.313599 0.099796
|
||||
1.800000 1100.313599 0.099796
|
||||
1.900000 1100.313599 0.099787
|
||||
2.000000 1100.313599 0.099821
|
||||
2.100000 1100.313599 0.099821
|
||||
2.200000 1100.313599 0.099812
|
||||
2.300000 1100.313599 0.099829
|
||||
2.400000 1100.313599 0.099829
|
||||
2.500000 1100.313599 0.099838
|
||||
2.600000 1100.313599 0.099854
|
||||
2.700000 1100.313599 0.099863
|
||||
2.800000 1100.313599 0.099854
|
||||
2.900000 1100.313599 0.099846
|
||||
3.000000 1100.313599 0.099863
|
||||
3.100000 1100.313599 0.099871
|
||||
3.200000 1100.313599 0.099879
|
||||
3.300000 1100.313599 0.099888
|
||||
3.400000 1100.313599 0.099896
|
||||
3.500000 1100.313599 0.099913
|
||||
3.600000 1100.313599 0.099930
|
||||
3.700000 1100.313599 0.099938
|
||||
3.800000 1100.313599 0.099930
|
||||
3.900000 1100.313599 0.099947
|
||||
4.000000 1100.313599 0.099963
|
||||
4.100000 1100.313599 0.099947
|
||||
4.200000 1100.313599 0.099980
|
||||
4.300000 1100.313599 0.099997
|
||||
4.400000 1100.313599 0.099972
|
||||
4.500000 1100.313599 0.099947
|
||||
4.600000 1100.313599 0.099947
|
||||
4.700000 1100.313599 0.099955
|
||||
4.800000 1100.313599 0.099955
|
||||
4.900000 1100.313599 0.099989
|
||||
5.000000 1100.313599 0.099997
|
||||
5.100000 1100.313599 0.099980
|
||||
5.200000 1100.313599 0.099989
|
||||
5.300000 1100.313599 0.099963
|
||||
5.400000 1100.313599 0.099980
|
||||
5.500000 1100.313599 0.099989
|
||||
5.600000 1100.313599 0.100005
|
||||
5.700000 1100.313599 0.099963
|
||||
5.800000 1100.313599 0.099980
|
||||
5.900000 1100.313599 0.099980
|
||||
6.000000 1100.313599 0.099980
|
||||
6.100000 1100.313599 0.099972
|
||||
6.200000 1100.313599 0.099997
|
||||
6.300000 1100.313599 0.099972
|
||||
6.400000 1100.313599 0.099955
|
||||
6.500000 1100.313599 0.099972
|
||||
6.600000 1100.313599 0.099963
|
||||
6.700000 1100.313599 0.099980
|
||||
6.800000 1100.313599 0.099963
|
||||
6.900000 1100.313599 0.099955
|
||||
7.000000 1100.313599 0.099989
|
||||
7.100000 1100.313599 0.099989
|
||||
7.200000 1100.313599 0.099980
|
||||
7.300000 1100.313599 0.099980
|
||||
7.400000 1100.313599 0.099980
|
||||
7.500000 1100.313599 0.099963
|
||||
7.600000 1100.313599 0.099980
|
||||
7.700000 1100.313599 0.099955
|
||||
7.800000 1100.313599 0.099980
|
||||
7.900000 1100.313599 0.099963
|
||||
8.000000 1100.313599 0.099989
|
||||
8.100000 1100.313599 0.099980
|
||||
8.200000 1100.313599 0.099989
|
||||
8.300000 1100.313599 0.099989
|
||||
8.400000 1100.313599 0.099972
|
||||
8.500000 1100.313599 0.100005
|
||||
8.600000 1100.313599 0.099980
|
||||
8.700000 1100.313599 0.100005
|
||||
8.800000 1100.313599 0.099997
|
||||
@@ -1,49 +0,0 @@
|
||||
Time(s) Zre(Ohm) Zim(Ohm) Z(Ohm) Freq(Hz) Phase(deg) EDC(mV)
|
||||
0.128000 6755.462891 3996.161133 7848.922363 10000.000000 -30.606230 0.000000
|
||||
0.272000 6824.638672 3994.455811 7907.677734 8888.900391 -30.340406 0.000000
|
||||
0.436571 7059.030762 4099.418457 8163.035156 7777.799805 -30.145187 0.000000
|
||||
0.628570 7516.110840 4371.320312 8694.846680 6666.700195 -30.181999 0.000000
|
||||
Time(s) Zre(Ohm) Zim(Ohm) Z(Ohm) Freq(Hz) Phase(deg) EDC(mV)
|
||||
0.128000 4004.078857 1725.499268 4360.045410 10000.000000 -23.312920 0.000000
|
||||
0.272000 3751.894043 1407.421021 4007.186523 8888.900391 -20.562214 0.000000
|
||||
0.436571 3795.918457 1347.885742 4028.125244 7777.799805 -19.549349 0.000000
|
||||
0.628570 3988.254883 1406.353516 4228.948730 6666.700195 -19.423862 0.000000
|
||||
0.858968 4340.244629 1560.648071 4612.303711 5555.600098 -19.777370 0.000000
|
||||
1.146964 4953.287598 1795.551514 5268.687012 4444.500000 -19.925425 0.000000
|
||||
1.530957 5583.770020 2229.389648 6012.375977 3333.399902 -21.764971 0.000000
|
||||
2.106936 6545.107422 2367.948486 6960.288086 2222.300049 -19.889572 0.000000
|
||||
3.258844 7567.829590 1800.241089 7779.004395 1111.199951 -13.380868 0.000000
|
||||
23.258844 7841.500488 363.667908 7849.928711 0.100000 -2.655323 0.000000
|
||||
Time(s) Zre(Ohm) Zim(Ohm) Z(Ohm) Freq(Hz) Phase(deg) EDC(mV)
|
||||
0.128000 4585.909668 1983.974854 4996.671387 10000.000000 -23.394470 0.000000
|
||||
0.272000 4468.743652 1882.592163 4849.105469 8888.900391 -22.844717 0.000000
|
||||
0.436571 4585.427246 1862.162964 4949.120605 7777.799805 -22.102295 0.000000
|
||||
0.628570 4762.757812 1835.767822 5104.302734 6666.700195 -21.078768 0.000000
|
||||
0.858968 5040.085449 1868.267578 5375.210449 5555.600098 -20.338821 0.000000
|
||||
1.146964 5369.222168 2009.491211 5732.939941 4444.500000 -20.518923 0.000000
|
||||
1.530957 5790.665039 2128.829590 6169.580078 3333.399902 -20.185007 0.000000
|
||||
2.106936 6104.018066 1879.055054 6386.695801 2222.300049 -17.110409 0.000000
|
||||
3.258844 6228.028809 1268.160278 6355.830078 1111.199951 -11.509306 0.000000
|
||||
23.258844 6003.355957 413.043579 6017.548340 0.100000 -3.935868 0.000000
|
||||
Time(s) Zre(Ohm) Zim(Ohm) Z(Ohm) Freq(Hz) Phase(deg) EDC(mV)
|
||||
0.128000 3572.804688 1110.373413 3741.371826 10000.000000 -17.264450 0.000000
|
||||
0.272000 3733.689209 1029.676392 3873.069580 8888.900391 -15.417786 0.000000
|
||||
0.436571 3814.144043 1035.226807 3952.137207 7777.799805 -15.185266 0.000000
|
||||
0.628570 3763.235596 993.421204 3892.149414 6666.700195 -14.787639 0.000000
|
||||
0.858968 3746.752197 922.539612 3858.656738 5555.600098 -13.832431 0.000000
|
||||
1.146964 3835.638184 876.370972 3934.481689 4444.500000 -12.870085 0.000000
|
||||
1.530957 3879.458496 808.279602 3962.765869 3333.399902 -11.769126 0.000000
|
||||
2.106936 4037.450439 756.480225 4107.708496 2222.300049 -10.612228 0.000000
|
||||
3.258844 4497.190430 741.943848 4557.982422 1111.199951 -9.368237 0.000000
|
||||
23.258844 5525.440430 743.580078 5575.249023 0.100000 -7.664470 0.000000
|
||||
Time(s) Zre(Ohm) Zim(Ohm) Z(Ohm) Freq(Hz) Phase(deg) EDC(mV)
|
||||
0.128000 4508.749023 1480.736694 4745.671387 10000.000000 -18.180910 0.000000
|
||||
0.272000 4432.497070 1333.723267 4628.806152 8888.900391 -16.746363 0.000000
|
||||
0.436571 4782.034668 1541.624512 5024.386719 7777.799805 -17.868198 0.000000
|
||||
0.628570 5205.424805 1835.522095 5519.563965 6666.700195 -19.423525 0.000000
|
||||
0.858968 5297.061035 1830.112549 5604.298828 5555.600098 -19.059818 0.000000
|
||||
1.146964 5442.899414 1763.836304 5721.562012 4444.500000 -17.955534 0.000000
|
||||
1.530957 5554.852051 1871.875732 5861.766113 3333.399902 -18.622803 0.000000
|
||||
2.106936 5667.162598 1750.656982 5931.402344 2222.300049 -17.166569 0.000000
|
||||
3.258844 5834.244629 1325.616577 5982.948242 1111.199951 -12.801015 0.000000
|
||||
23.258844 5939.145508 584.428345 5967.831055 0.100000 -5.619970 0.000000
|
||||
@@ -1,55 +0,0 @@
|
||||
Time(s) Zre(Ω) Zim(Ω) Z(Ω) Freq(Hz) Phase(°) EDC(V)
|
||||
0.128000 8836.014648 4088.676270 9736.140625 10000.000000 -24.831327 0.000000
|
||||
0.289142 8947.045898 3906.468262 9762.690430 7943.282227 -23.587093 0.000000
|
||||
0.492009 9445.424805 4166.807129 10323.678711 6309.573242 -23.804461 0.000000
|
||||
0.747402 10144.289062 4494.853516 11095.508789 5011.872070 -23.897749 0.000000
|
||||
1.068924 10604.609375 4514.041992 11525.376953 3981.071533 -23.057899 0.000000
|
||||
1.473696 11142.434570 4558.653809 12038.902344 3162.277588 -22.250721 0.000000
|
||||
1.983273 11883.416016 4958.137695 12876.284180 2511.886230 -22.647444 0.000000
|
||||
2.624793 12497.970703 5309.595215 13579.067383 1995.262329 -23.017561 0.000000
|
||||
3.432418 13203.150391 5649.524414 14361.069336 1584.893188 -23.165701 0.000000
|
||||
4.449158 14128.726562 6126.426270 15399.805664 1258.925293 -23.442274 0.000000
|
||||
4.481158 14911.369141 6479.161133 16258.181641 1000.000000 -23.485498 0.000000
|
||||
4.521444 15656.686523 6829.202637 17081.271484 794.328247 -23.566080 0.000000
|
||||
4.572160 16835.632812 7643.918945 18489.673828 630.957336 -24.419577 0.000000
|
||||
4.636009 18222.216797 8657.716797 20174.371094 501.187225 -25.413269 0.000000
|
||||
4.716389 22157.533203 11829.420898 25117.552734 398.107178 -28.096781 0.000000
|
||||
4.817582 23357.978516 11746.929688 26145.468750 316.227753 -26.698219 0.000000
|
||||
4.944976 24984.910156 12806.191406 28075.687500 251.188629 -27.137728 0.000000
|
||||
4.716389 20656.267578 10043.393555 22968.482422 398.107178 -25.929729 0.000000
|
||||
4.817582 23355.941406 11758.861328 26149.011719 316.227753 -26.723581 0.000000
|
||||
4.944976 24946.281250 12709.506836 27997.294922 251.188629 -26.997705 0.000000
|
||||
5.105356 25699.732422 12844.353516 28730.708984 199.526230 -26.555218 0.000000
|
||||
5.307262 28007.763672 13781.557617 31214.837891 158.489319 -26.200048 0.000000
|
||||
5.561448 32278.408203 15294.345703 35718.519531 125.892532 -25.352806 0.000000
|
||||
5.601448 36631.953125 16377.120117 40126.175781 100.000000 -24.088081 0.000000
|
||||
5.651804 36212.464844 14175.981445 38888.316406 79.432823 -21.378698 0.000000
|
||||
5.715200 31005.900391 10025.936523 32586.580078 63.095734 -17.918913 0.000000
|
||||
5.795011 27789.210938 7751.479004 28850.054688 50.118721 -15.585830 0.000000
|
||||
5.895486 35634.070312 9420.209961 36858.207031 39.810719 -14.807939 0.000000
|
||||
6.021977 42739.617188 10889.406250 44105.035156 31.622778 -14.293976 0.000000
|
||||
6.181220 41334.429688 12303.883789 43126.796875 25.118862 -16.576517 0.000000
|
||||
6.381695 44635.375000 13761.085938 46708.503906 19.952623 -17.134529 0.000000
|
||||
6.634078 47430.500000 9581.280273 48388.566406 15.848931 -11.420444 0.000000
|
||||
6.951809 47006.734375 6501.482910 47454.214844 12.589253 -7.874598 0.000000
|
||||
7.351809 47260.820312 5934.597168 47631.968750 10.000000 -7.157238 0.000000
|
||||
7.855379 51975.855469 140.047073 51976.042969 7.943282 -0.154381 0.000000
|
||||
8.489336 58372.769531 2538.466309 58427.937500 6.309573 -2.490062 0.000000
|
||||
9.287441 59285.113281 8795.745117 59934.046875 5.011872 -8.439040 0.000000
|
||||
10.292196 55917.351562 9934.631836 56793.019531 3.981072 -10.074409 0.000000
|
||||
11.557107 51669.527344 1365.285522 51687.562500 3.162278 -1.513598 0.000000
|
||||
13.149536 nan nan nan 2.511886 2.248806 0.000000
|
||||
15.154285 nan nan nan 1.995262 4.537589 0.000000
|
||||
17.678116 nan nan nan 1.584893 1.901186 0.000000
|
||||
20.855431 nan nan nan 1.258925 -14.520571 0.000000
|
||||
22.855431 nan nan nan 1.000000 -21.347967 0.000000
|
||||
25.373281 nan nan nan 0.794328 -16.286789 0.000000
|
||||
28.543070 43693.843750 2706.736816 43777.601562 0.630957 -3.544817 0.000000
|
||||
32.533596 34982.421875 -4359.896973 35253.062500 0.501187 7.104203 0.000000
|
||||
37.557373 30128.955078 -116.846642 30129.181641 0.398107 0.222204 0.000000
|
||||
43.881924 32771.234375 6115.649902 33336.992188 0.316228 -10.570740 0.000000
|
||||
51.844055 18206.365234 -7035.584961 19518.482422 0.251189 21.128300 0.000000
|
||||
61.867813 -1157.350342 -5457.016602 5578.395020 0.199526 101.974136 0.000000
|
||||
74.486984 -7572.944336 -5757.060547 9512.792969 0.158489 142.757385 0.000000
|
||||
90.373489 -6719.222656 -12092.010742 13833.461914 0.125893 119.059814 0.000000
|
||||
110.373489 2759.366699 -3812.787109 4706.532715 0.100000 54.106220 0.000000
|
||||
@@ -1,5 +0,0 @@
|
||||
Time(s) Zre(Ohm) Zim(Ohm) Z(Ohm) Freq(Hz) Phase(deg) EDC(mV)
|
||||
0.128000 5301.714355 1606.345947 5539.722168 10000.000000 -16.856081 0.000000
|
||||
0.272000 5303.290527 1594.772095 5537.886719 8888.900391 -16.736752 0.000000
|
||||
0.436571 5347.800781 1595.693970 5580.789551 7777.799805 -16.614220 0.000000
|
||||
0.628570 5478.396973 1635.483032 5717.310547 6666.700195 -16.622107 0.000000
|
||||
@@ -1,70 +0,0 @@
|
||||
initialize: True
|
||||
get_machine_ids: {'success': True, 'machine_ids': [1, 3, 2]}
|
||||
version: reflect_linear_scan_signature
|
||||
linear_scan_related_methods:
|
||||
Add_ConstantPotential:['System.String', 'System.Single', 'System.Int32', 'System.Boolean', 'System.Boolean', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['parentTag', 'Voltage', 'VoltageVSType', 'IsAccrueQ', 'isVoltageTrig', 'VoltageOrCurrentTrigDirection', 'VoltageOrCurrentTrigValue', 'capacityTrigDirection', 'capacityTrigValue', 'timePerPoint', 'continueTime', 'IsUseResolution', 'Resolution', 'isUseDeltaI', 'deltaI', 'isUseDeltaQ', 'deltaQ', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_Circle_Voltammetry_Multi:['System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['IsUseInitialPotential', 'InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'IsUseFinallyPotential', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Staircase_Cyclic_Voltammetry_MultipleCycles:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Linear_Scan_Voltammetry:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Linear_Scan_Voltammetry_New:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'voltageInterval', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Circle_Voltammetry_Single:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['InitialPotential', 'InitialPotentialVSType', 'TopPotential', 'TopPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Staircase_Linear_Scan:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Potentiodynamic:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Staircase_Cyclic_Voltammetry_Single:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'TopPotential', 'TopPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_ChronoamperonetryParam:['System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'InitialPotential', 'InitialPotentialVSType', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_ChronopotentiometryParam:['System.Single', 'System.Single', 'System.Single', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'current', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_ChronocoulometryParam:['System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'potential', 'potentialotentialVSType', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_ConstantPotential:['System.Single', 'System.Int32', 'System.Boolean', 'System.Boolean', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['Voltage', 'VoltageVSType', 'IsAccrueQ', 'isVoltageTrig', 'VoltageOrCurrentTrigDirection', 'VoltageOrCurrentTrigValue', 'capacityTrigDirection', 'capacityTrigValue', 'timePerPoint', 'continueTime', 'IsUseResolution', 'Resolution', 'isUseDeltaI', 'deltaI', 'isUseDeltaQ', 'deltaQ', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_Potentiostatic_Staircase:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['OutputValue1', 'vsType1', 'OutputValue2', 'vsType2', 'OutputValue3', 'vsType3', 'timePerPoint', 'stepCount', 'measureDelay', 'plusTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Fast_Potential_Pulses:['System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['plusCount', 'OutputValue1', 'ContinueTime1', 'vsType1', 'OutputValue2', 'ContinueTime2', 'vsType2', 'OutputValue3', 'ContinueTime3', 'vsType3', 'OutputValue4', 'ContinueTime4', 'vsType4', 'OutputValue5', 'ContinueTime5', 'vsType5', 'timePerPoint', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
StartConstantPotential_LineScan:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StaticTime', 'CycleCount', 'TimePerPoint', 'ContinueTime', 'StepHeight', 'SweptRate', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Circle_Voltammetry_Multi_New:['System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['voltageInterval', 'IsUseInitialPotential', 'InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'IsUseFinallyPotential', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Staircase_Cyclic_Voltammetry_MultipleCycles_New:['System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['IsUseInitialPotential', 'InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'IsUseFinallyPotential', 'FinallyPotential', 'FinallyPotentialVSType', 'stepHeight', 'stepTime', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
all_start_methods:
|
||||
Start_Circle_Voltammetry_Multi:['System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['IsUseInitialPotential', 'InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'IsUseFinallyPotential', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Staircase_Cyclic_Voltammetry_MultipleCycles:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_SplitLPR:['System.Single', 'System.Single', 'ECCore.VSType', 'System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['aNodeVoltage', 'cathodeVoltage', 'aNodeVSTye', 'stepHeight', 'stepTime', 'continueTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_OpenCircuit:['System.Single', 'System.Single', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'machineId']
|
||||
Start_Galvanic_Corrosion:['System.Single', 'System.Single', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'machineId']
|
||||
Start_Electrochemical_Noise_En:['System.Single', 'System.Single', 'System.Int32', 'System.Int32'] | names=['timePerPoint', 'timePerSegment', 'segmentCount', 'machineId']
|
||||
Start_Linear_Scan_Voltammetry:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Linear_Scan_Voltammetry_New:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'voltageInterval', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Circle_Voltammetry_Single:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['InitialPotential', 'InitialPotentialVSType', 'TopPotential', 'TopPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Staircase_Linear_Scan:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Potentiodynamic:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Galvanodynamic:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Staircase_Cyclic_Voltammetry_Single:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'TopPotential', 'TopPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Cyclic_Polarization:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.String', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'TopPotential', 'TopPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'voltageTrigValue', 'currentTrigValue', 'machineId']
|
||||
Start_ChronoamperonetryParam:['System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'InitialPotential', 'InitialPotentialVSType', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_ChronopotentiometryParam:['System.Single', 'System.Single', 'System.Single', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'current', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_ChronocoulometryParam:['System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'potential', 'potentialotentialVSType', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_SquareWare:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['initialPotential', 'initialPotentialVSType', 'finallyPotential', 'finallyPotentialVSType', 'stepHeight', 'pluseHeight', 'startFreq', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_DifferentialPulse:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['initialPotential', 'initialPotentialVSType', 'finallyPotential', 'finallyPotentialVSType', 'stepHeight', 'pluseHeight', 'pluseWidth', 'stepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_NormalPulse:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['initialPotential', 'initialPotentialVSType', 'finallyPotential', 'finallyPotentialVSType', 'stepHeight', 'pluseWidth', 'stepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_EIS:['System.Boolean', 'System.Double', 'System.Double', 'System.Single', 'System.Int32', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single', 'System.Int32'] | names=['isVolEIS', 'StartFreq', 'EndFreq', 'Amplitude', 'IntervalType', 'PointCount', 'Voltage', 'VoltageVSType', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime', 'dataQuality']
|
||||
Start_EnergyOpenCircuit:['System.Boolean', 'System.Single', 'System.Boolean', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['isUseExcursionRate', 'excursionRate', 'isVoltageTrig', 'VoltageOrCurrentTrigDirection', 'VoltageOrCurrentTrigValue', 'capacityTrigDirection', 'capacityTrigValue', 'timePerPoint', 'continueTime', 'IsUseResolution', 'Resolution', 'isUseDeltaI', 'deltaI', 'isUseDeltaQ', 'deltaQ', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_ConstantPotential:['System.Single', 'System.Int32', 'System.Boolean', 'System.Boolean', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['Voltage', 'VoltageVSType', 'IsAccrueQ', 'isVoltageTrig', 'VoltageOrCurrentTrigDirection', 'VoltageOrCurrentTrigValue', 'capacityTrigDirection', 'capacityTrigValue', 'timePerPoint', 'continueTime', 'IsUseResolution', 'Resolution', 'isUseDeltaI', 'deltaI', 'isUseDeltaQ', 'deltaQ', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_ConstantContent:['System.Single', 'System.Boolean', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['current', 'isVoltageTrig', 'VoltageOrCurrentTrigDirection', 'VoltageOrCurrentTrigValue', 'capacityTrigDirection', 'capacityTrigValue', 'timePerPoint', 'continueTime', 'IsUseResolution', 'Resolution', 'isUseDeltaI', 'deltaI', 'isUseDeltaQ', 'deltaQ', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_CurrentChargeDisCharge:['System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Int32', 'ECCore.SwithPriority', 'ECCore.TimeType', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['CurrentValue1', 'TimeLimitValue1', 'Vol1LimitValue1', 'CurrentValue2', 'TimeLimitValue2', 'Vol1LimitValue2', 'timePerPoint', 'CycleCount', 'CurrentSwithPriority', 'timeType', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_ConstantPower:['System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['power', 'isCharge', 'currentUpLimit', 'isVoltageTrig', 'VoltageOrCurrentTrigDirection', 'VoltageOrCurrentTrigValue', 'capacityTrigDirection', 'capacityTrigValue', 'timePerPoint', 'continueTime', 'IsUseResolution', 'Resolution', 'isUseDeltaI', 'deltaI', 'isUseDeltaQ', 'deltaQ', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_ConstantResistance:['System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['resistance', 'isCharge', 'currentUpLimit', 'isVoltageTrig', 'VoltageOrCurrentTrigDirection', 'VoltageOrCurrentTrigValue', 'capacityTrigDirection', 'capacityTrigValue', 'timePerPoint', 'continueTime', 'IsUseResolution', 'Resolution', 'isUseDeltaI', 'deltaI', 'isUseDeltaQ', 'deltaQ', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'machineId']
|
||||
Start_Tafel:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_LPR:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['InitialPotential', 'InitialPotentialVSType', 'FinallyPotential', 'FinallyPotentialVSType', 'StepHeight', 'StepTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Zra:['System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['timePerPoint', 'continueTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Potentiostatic_Staircase:['System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['OutputValue1', 'vsType1', 'OutputValue2', 'vsType2', 'OutputValue3', 'vsType3', 'timePerPoint', 'stepCount', 'measureDelay', 'plusTime', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Fast_Potential_Pulses:['System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['plusCount', 'OutputValue1', 'ContinueTime1', 'vsType1', 'OutputValue2', 'ContinueTime2', 'vsType2', 'OutputValue3', 'ContinueTime3', 'vsType3', 'OutputValue4', 'ContinueTime4', 'vsType4', 'OutputValue5', 'ContinueTime5', 'vsType5', 'timePerPoint', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Fast_Galvanic_Pulses:['System.Int32', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32'] | names=['plusCount', 'OutputValue1', 'ContinueTime1', 'OutputValue2', 'ContinueTime2', 'OutputValue3', 'ContinueTime3', 'OutputValue4', 'ContinueTime4', 'OutputValue5', 'ContinueTime5', 'timePerPoint', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId']
|
||||
Start_Circle_Voltammetry_Multi_New:['System.Single', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['voltageInterval', 'IsUseInitialPotential', 'InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'IsUseFinallyPotential', 'FinallyPotential', 'FinallyPotentialVSType', 'ScanRate', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_Staircase_Cyclic_Voltammetry_MultipleCycles_New:['System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Single', 'System.Int32', 'System.Boolean', 'System.Single', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.Single'] | names=['IsUseInitialPotential', 'InitialPotential', 'InitialPotentialVSType', 'TopPotential1', 'TopPotential1VSType', 'TopPotential2', 'TopPotential2VSType', 'IsUseFinallyPotential', 'FinallyPotential', 'FinallyPotentialVSType', 'stepHeight', 'stepTime', 'cycleCount', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'machineId', 'delayTime']
|
||||
Start_OpenCircuit_All:['System.Single', 'System.Single'] | names=['timePerPoint', 'continueTime']
|
||||
Start_EIS_All:['System.Boolean', 'System.Double', 'System.Double', 'System.Single', 'System.Int32', 'System.Int32', 'System.Single', 'System.Single', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Int32', 'System.String', 'System.Single', 'System.Int32'] | names=['isVolEIS', 'StartFreq', 'EndFreq', 'Amplitude', 'IntervalType', 'PointCount', 'Voltage', 'VoltageVSType', 'isVoltageRandAuto', 'VoltageRand', 'isCurrentRandAuto', 'currentRand', 'isVoltageFilterAuto', 'VoltageFilter', 'isCurrentFilterAuto', 'currentFilter', 'delayTime', 'dataQuality']
|
||||
version: lsv_v1
|
||||
version: lsv_v1
|
||||
start_cv_params: {'is_use_initial_potential': True, 'initial_potential': -1.0, 'initial_potential_vs_type': 0, 'top_potential1': 1.0, 'top_potential1_vs_type': 0, 'top_potential2': -2.0, 'top_potential2_vs_type': 0, 'is_use_finally_potential': True, 'finally_potential': -1.0, 'finally_potential_vs_type': 0, 'scan_rate': 0.2, 'cycle_count': 2, 'is_voltage_rand_auto': 1, 'voltage_rand': '1000', 'is_current_rand_auto': 1, 'current_rand': '1000', 'is_voltage_filter_auto': 1, 'voltage_filter': '10Hz', 'is_current_filter_auto': 1, 'current_filter': '10Hz', 'machine_id': 2}
|
||||
start_cv: {'success': True, 'return_info': 'ok', 'machine_id': 2}
|
||||
start_realtime_output_cv: {'success': True, 'running': True, 'machine_id': 2, 'file': 'D:\\Uni-Lab-OS\\Uni-Lab-OS\\unilabos\\devices\\donghua_ec\\x64release\\DHInterface\\SourceData\\2025-12-12\\循环伏安\\2号机(循环伏安(多循环)).txt'}
|
||||
result_types: [0]
|
||||
data_details: [{'cand': 0, 'len': 0, 'd3': (0, 0, 0), 'd4': (0, 0, 0, 0), 'd7': (0, 0, 0, 0, 0, 0, 0)}]
|
||||
stop_realtime_output_cv: {'success': True, 'machine_id': 2, 'file': 'D:\\Uni-Lab-OS\\Uni-Lab-OS\\unilabos\\devices\\donghua_ec\\x64release\\DHInterface\\SourceData\\2025-12-12\\循环伏安\\2号机(循环伏安(多循环)).txt'}
|
||||
export_cyclic_voltammetry: {'success': True, 'files': ['D:\\Uni-Lab-OS\\Uni-Lab-OS\\unilabos\\devices\\donghua_ec\\exports\\循环伏安\\2号机(循环伏安(多循环)).txt'], 'dest': 'D:\\Uni-Lab-OS\\Uni-Lab-OS\\unilabos\\devices\\donghua_ec\\exports\\循环伏安'}
|
||||
stop_experiment_cv: {'success': True, 'machine_id': 2}
|
||||
@@ -1,176 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure repository is on PYTHONPATH
|
||||
repo_root = Path(__file__).resolve().parents[3]
|
||||
sys.path.append(str(repo_root))
|
||||
|
||||
from unilabos.devices.donghua_ec.donghua_ec import DonghuaEC
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
async def main():
|
||||
iface_dir = Path(__file__).resolve().parent / "x64release" / "DHInterface"
|
||||
drv = DonghuaEC(config={"interface_dir": str(iface_dir)})
|
||||
ok = await drv.initialize()
|
||||
res = drv.get_machine_ids()
|
||||
import time as _t
|
||||
out = Path(__file__).resolve().parent / "init_output.txt"
|
||||
try:
|
||||
if out.exists():
|
||||
out.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
with open(out, "w", encoding="utf-8") as f:
|
||||
f.write(f"initialize: {ok}\n")
|
||||
f.write(f"get_machine_ids: {res}\n")
|
||||
f.write("version: reflect_linear_scan_signature\n")
|
||||
try:
|
||||
import System
|
||||
elec = drv._elec
|
||||
t = elec.GetType()
|
||||
sig_lines = []
|
||||
for m in t.GetMethods():
|
||||
name = m.Name
|
||||
if ("Linear" in name) or ("Scan" in name) or ("Volt" in name) or ("Chrono" in name) or ("Potent" in name):
|
||||
ps_types = [str(p.ParameterType.FullName) for p in m.GetParameters()]
|
||||
ps_names = [str(p.Name) for p in m.GetParameters()]
|
||||
sig_lines.append(name + ":" + str(ps_types) + " | names=" + str(ps_names))
|
||||
f.write("linear_scan_related_methods:\n")
|
||||
for ln in sig_lines:
|
||||
f.write(ln + "\n")
|
||||
# dump all Start_* signatures for debugging
|
||||
all_start = []
|
||||
for m in t.GetMethods():
|
||||
name = m.Name
|
||||
if name.startswith("Start_"):
|
||||
ps_types = [str(p.ParameterType.FullName) for p in m.GetParameters()]
|
||||
ps_names = [str(p.Name) for p in m.GetParameters()]
|
||||
all_start.append(name + ":" + str(ps_types) + " | names=" + str(ps_names))
|
||||
f.write("all_start_methods:\n")
|
||||
for ln in all_start:
|
||||
f.write(ln + "\n")
|
||||
except Exception as e:
|
||||
f.write(f"reflect_error: {e}\n")
|
||||
f.write("version: lsv_v1\n")
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write("version: lsv_v1\n")
|
||||
# Cyclic Voltammetry (Multi)
|
||||
cv_params = dict(
|
||||
is_use_initial_potential=True,
|
||||
initial_potential=-1.0,
|
||||
initial_potential_vs_type=0,
|
||||
top_potential1=1.0,
|
||||
top_potential1_vs_type=0,
|
||||
top_potential2=-2.0,
|
||||
top_potential2_vs_type=0,
|
||||
is_use_finally_potential=True,
|
||||
finally_potential=-1.0,
|
||||
finally_potential_vs_type=0,
|
||||
scan_rate=0.2,
|
||||
cycle_count=2,
|
||||
is_voltage_rand_auto=1,
|
||||
voltage_rand="1000",
|
||||
is_current_rand_auto=1,
|
||||
current_rand="1000",
|
||||
is_voltage_filter_auto=1,
|
||||
voltage_filter="10Hz",
|
||||
is_current_filter_auto=1,
|
||||
current_filter="10Hz",
|
||||
machine_id=2,
|
||||
)
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"start_cv_params: {cv_params}\n")
|
||||
try:
|
||||
start_cv = drv.start_cyclic_voltammetry_multi(**cv_params)
|
||||
except Exception as e:
|
||||
start_cv = {"success": False, "return_info": str(e), "machine_id": 2}
|
||||
try:
|
||||
if not start_cv.get("success"):
|
||||
drv.data["last_result_type"] = "cyclic_voltammetry"
|
||||
except Exception:
|
||||
pass
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"start_cv: {start_cv}\n")
|
||||
try:
|
||||
rt4 = drv.start_realtime_output(machine_id=2, interval=0.5)
|
||||
except Exception as e:
|
||||
rt4 = {"success": False, "error": str(e)}
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"start_realtime_output_cv: {rt4}\n")
|
||||
try:
|
||||
import System
|
||||
elec = drv._elec
|
||||
rts = []
|
||||
try:
|
||||
rts = list(elec.GetResultDataType(2))
|
||||
except Exception:
|
||||
rts = []
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"result_types: {rts}\n")
|
||||
print("result_types:", rts)
|
||||
details = []
|
||||
for cand in rts:
|
||||
try:
|
||||
s = elec.GetData(cand, 2)
|
||||
except Exception:
|
||||
s = None
|
||||
d = {"cand": cand, "len": 0, "d3": (), "d4": (), "d7": ()}
|
||||
try:
|
||||
it = list(s)
|
||||
d["len"] = len(it)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
a0 = list(elec.SplitData(s, 3, 0)); a1 = list(elec.SplitData(s, 3, 1)); a2 = list(elec.SplitData(s, 3, 2))
|
||||
d["d3"] = (len(a0), len(a1), len(a2))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
b0 = list(elec.SplitData(s, 4, 0)); b1 = list(elec.SplitData(s, 4, 1)); b2 = list(elec.SplitData(s, 4, 2)); b3 = list(elec.SplitData(s, 4, 3))
|
||||
d["d4"] = (len(b0), len(b1), len(b2), len(b3))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
c0 = list(elec.SplitData(s, 7, 0)); c1 = list(elec.SplitData(s, 7, 1)); c2 = list(elec.SplitData(s, 7, 2)); c3 = list(elec.SplitData(s, 7, 3)); c4 = list(elec.SplitData(s, 7, 4)); c5 = list(elec.SplitData(s, 7, 5)); c6 = list(elec.SplitData(s, 7, 6))
|
||||
d["d7"] = (len(c0), len(c1), len(c2), len(c3), len(c4), len(c5), len(c6))
|
||||
except Exception:
|
||||
pass
|
||||
details.append(d)
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"data_details: {details}\n")
|
||||
print("data_details:", details)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
import time as _t
|
||||
_t.sleep(10.0)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
stop_rt4 = drv.stop_realtime_output(machine_id=2)
|
||||
except Exception as e:
|
||||
stop_rt4 = {"success": False, "error": str(e)}
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"stop_realtime_output_cv: {stop_rt4}\n")
|
||||
try:
|
||||
exp_cv = drv.export_cyclic_voltammetry_data(machine_id=2, dest_dir=str(Path(__file__).resolve().parent / "exports" / "循环伏安"))
|
||||
except Exception as e:
|
||||
exp_cv = {"success": False, "error": str(e)}
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"export_cyclic_voltammetry: {exp_cv}\n")
|
||||
try:
|
||||
stop_cv = drv.stop_experiment(machine_id=2)
|
||||
except Exception as e:
|
||||
stop_cv = {"success": False, "error": str(e)}
|
||||
with open(out, "a", encoding="utf-8") as f:
|
||||
f.write(f"stop_experiment_cv: {stop_cv}\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except Exception as e:
|
||||
print("error:", e)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user