mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-24 04:10:33 +00:00
add virtual_sample_demo 样品追踪测试设备
This commit is contained in:
88
unilabos/devices/virtual/virtual_sample_demo.py
Normal file
88
unilabos/devices/virtual/virtual_sample_demo.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""虚拟样品演示设备 — 用于前端 sample tracking 功能的极简 demo"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
class VirtualSampleDemo:
|
||||
"""虚拟样品追踪演示设备,提供两种典型返回模式:
|
||||
- measure_samples: 等长输入输出 (前端按 index 自动对齐)
|
||||
- split_and_measure: 输出比输入长,附带 samples 列标注归属
|
||||
"""
|
||||
|
||||
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
|
||||
if device_id is None and "id" in kwargs:
|
||||
device_id = kwargs.pop("id")
|
||||
if config is None and "config" in kwargs:
|
||||
config = kwargs.pop("config")
|
||||
|
||||
self.device_id = device_id or "unknown_sample_demo"
|
||||
self.config = config or {}
|
||||
self.logger = logging.getLogger(f"VirtualSampleDemo.{self.device_id}")
|
||||
self.data: Dict[str, Any] = {"status": "Idle"}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Action 1: 等长输入输出,无 samples 列
|
||||
# ------------------------------------------------------------------
|
||||
async def measure_samples(self, concentrations: List[float]) -> Dict[str, Any]:
|
||||
"""模拟光度测量。absorbance = concentration * 0.05 + noise
|
||||
|
||||
入参和出参 list 长度相等,前端按 index 自动对齐。
|
||||
"""
|
||||
self.logger.info(f"measure_samples: concentrations={concentrations}")
|
||||
absorbance = [round(c * 0.05 + random.gauss(0, 0.005), 4) for c in concentrations]
|
||||
return {"concentrations": concentrations, "absorbance": absorbance}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Action 2: 输出比输入长,带 samples 列
|
||||
# ------------------------------------------------------------------
|
||||
async def split_and_measure(self, volumes: List[float], split_count: int = 3) -> Dict[str, Any]:
|
||||
"""将每个样品均分为 split_count 份后逐份测量。
|
||||
|
||||
返回的 list 长度 = len(volumes) * split_count,
|
||||
附带 samples 列标注每行属于第几个输入样品 (0-based index)。
|
||||
"""
|
||||
self.logger.info(f"split_and_measure: volumes={volumes}, split_count={split_count}")
|
||||
out_volumes: List[float] = []
|
||||
readings: List[float] = []
|
||||
samples: List[int] = []
|
||||
|
||||
for idx, vol in enumerate(volumes):
|
||||
split_vol = round(vol / split_count, 2)
|
||||
for _ in range(split_count):
|
||||
out_volumes.append(split_vol)
|
||||
readings.append(round(random.uniform(0.1, 1.0), 4))
|
||||
samples.append(idx)
|
||||
|
||||
return {"volumes": out_volumes, "readings": readings, "samples": samples}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Action 3: 入参和出参都带 samples 列(不等长)
|
||||
# ------------------------------------------------------------------
|
||||
async def analyze_readings(self, readings: List[float], samples: List[int]) -> Dict[str, Any]:
|
||||
"""对 split_and_measure 的输出做二次分析。
|
||||
|
||||
入参 readings/samples 长度相同但 > 原始样品数,
|
||||
出参同样带 samples 列,长度与入参一致。
|
||||
"""
|
||||
self.logger.info(f"analyze_readings: readings={readings}, samples={samples}")
|
||||
scores: List[float] = []
|
||||
passed: List[bool] = []
|
||||
threshold = 0.4
|
||||
|
||||
for r in readings:
|
||||
score = round(r * 100 + random.gauss(0, 2), 2)
|
||||
scores.append(score)
|
||||
passed.append(r >= threshold)
|
||||
|
||||
return {"scores": scores, "passed": passed, "samples": samples}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 状态属性
|
||||
# ------------------------------------------------------------------
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self.data.get("status", "Idle")
|
||||
@@ -2804,6 +2804,294 @@ virtual_rotavap:
|
||||
- vacuum_pressure
|
||||
type: object
|
||||
version: 1.0.0
|
||||
virtual_sample_demo:
|
||||
category:
|
||||
- virtual_device
|
||||
class:
|
||||
action_value_mappings:
|
||||
analyze_readings:
|
||||
feedback: {}
|
||||
goal:
|
||||
readings: readings
|
||||
samples: samples
|
||||
goal_default:
|
||||
readings: []
|
||||
samples: []
|
||||
handles:
|
||||
input:
|
||||
- data_key: readings
|
||||
data_source: handle
|
||||
data_type: sample_list
|
||||
handler_key: readings_in
|
||||
label: 测量读数
|
||||
- data_key: samples
|
||||
data_source: handle
|
||||
data_type: sample_index
|
||||
handler_key: samples_in
|
||||
label: 样品索引
|
||||
output:
|
||||
- data_key: scores
|
||||
data_source: executor
|
||||
data_type: sample_list
|
||||
handler_key: scores_out
|
||||
label: 分析得分
|
||||
- data_key: passed
|
||||
data_source: executor
|
||||
data_type: sample_list
|
||||
handler_key: passed_out
|
||||
label: 是否通过
|
||||
- data_key: samples
|
||||
data_source: executor
|
||||
data_type: sample_index
|
||||
handler_key: samples_result_out
|
||||
label: 样品索引
|
||||
placeholder_keys: {}
|
||||
result:
|
||||
passed: passed
|
||||
samples: samples
|
||||
scores: scores
|
||||
schema:
|
||||
description: 对 split_and_measure 输出做二次分析,入参和出参都带 samples 列
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: AnalyzeReadings_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
readings:
|
||||
description: 测量读数(来自 split_and_measure)
|
||||
items:
|
||||
type: number
|
||||
type: array
|
||||
samples:
|
||||
description: 每行归属的输入样品 index (0-based)
|
||||
items:
|
||||
type: integer
|
||||
type: array
|
||||
required:
|
||||
- readings
|
||||
- samples
|
||||
title: AnalyzeReadings_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
passed:
|
||||
description: 是否通过阈值
|
||||
items:
|
||||
type: boolean
|
||||
type: array
|
||||
samples:
|
||||
description: 每行归属的输入样品 index (0-based)
|
||||
items:
|
||||
type: integer
|
||||
type: array
|
||||
scores:
|
||||
description: 分析得分
|
||||
items:
|
||||
type: number
|
||||
type: array
|
||||
required:
|
||||
- scores
|
||||
- passed
|
||||
- samples
|
||||
title: AnalyzeReadings_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: AnalyzeReadings
|
||||
type: object
|
||||
type: UniLabJsonCommandAsync
|
||||
auto-cleanup:
|
||||
feedback: {}
|
||||
goal: {}
|
||||
goal_default: {}
|
||||
handles: {}
|
||||
placeholder_keys: {}
|
||||
result: {}
|
||||
schema:
|
||||
description: cleanup的参数schema
|
||||
properties:
|
||||
feedback: {}
|
||||
goal:
|
||||
properties: {}
|
||||
required: []
|
||||
type: object
|
||||
result: {}
|
||||
required:
|
||||
- goal
|
||||
title: cleanup参数
|
||||
type: object
|
||||
type: UniLabJsonCommandAsync
|
||||
measure_samples:
|
||||
feedback: {}
|
||||
goal:
|
||||
concentrations: concentrations
|
||||
goal_default:
|
||||
concentrations: []
|
||||
handles:
|
||||
output:
|
||||
- data_key: concentrations
|
||||
data_source: executor
|
||||
data_type: sample_list
|
||||
handler_key: concentrations_out
|
||||
label: 浓度列表
|
||||
- data_key: absorbance
|
||||
data_source: executor
|
||||
data_type: sample_list
|
||||
handler_key: absorbance_out
|
||||
label: 吸光度列表
|
||||
placeholder_keys: {}
|
||||
result:
|
||||
absorbance: absorbance
|
||||
concentrations: concentrations
|
||||
schema:
|
||||
description: 模拟光度测量,入参出参等长
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: MeasureSamples_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
concentrations:
|
||||
description: 样品浓度列表
|
||||
items:
|
||||
type: number
|
||||
type: array
|
||||
required:
|
||||
- concentrations
|
||||
title: MeasureSamples_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
absorbance:
|
||||
description: 吸光度列表(与浓度等长)
|
||||
items:
|
||||
type: number
|
||||
type: array
|
||||
concentrations:
|
||||
description: 原始浓度列表
|
||||
items:
|
||||
type: number
|
||||
type: array
|
||||
required:
|
||||
- concentrations
|
||||
- absorbance
|
||||
title: MeasureSamples_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: MeasureSamples
|
||||
type: object
|
||||
type: UniLabJsonCommandAsync
|
||||
split_and_measure:
|
||||
feedback: {}
|
||||
goal:
|
||||
split_count: split_count
|
||||
volumes: volumes
|
||||
goal_default:
|
||||
split_count: 3
|
||||
volumes: []
|
||||
handles:
|
||||
output:
|
||||
- data_key: readings
|
||||
data_source: executor
|
||||
data_type: sample_list
|
||||
handler_key: readings_out
|
||||
label: 测量读数
|
||||
- data_key: samples
|
||||
data_source: executor
|
||||
data_type: sample_index
|
||||
handler_key: samples_out
|
||||
label: 样品索引
|
||||
- data_key: volumes
|
||||
data_source: executor
|
||||
data_type: sample_list
|
||||
handler_key: volumes_out
|
||||
label: 均分体积
|
||||
placeholder_keys: {}
|
||||
result:
|
||||
readings: readings
|
||||
samples: samples
|
||||
volumes: volumes
|
||||
schema:
|
||||
description: 均分样品后逐份测量,输出带 samples 列标注归属
|
||||
properties:
|
||||
feedback:
|
||||
properties: {}
|
||||
required: []
|
||||
title: SplitAndMeasure_Feedback
|
||||
type: object
|
||||
goal:
|
||||
properties:
|
||||
split_count:
|
||||
description: 每个样品均分的份数
|
||||
type: integer
|
||||
volumes:
|
||||
description: 样品体积列表
|
||||
items:
|
||||
type: number
|
||||
type: array
|
||||
required:
|
||||
- volumes
|
||||
title: SplitAndMeasure_Goal
|
||||
type: object
|
||||
result:
|
||||
properties:
|
||||
readings:
|
||||
description: 测量读数
|
||||
items:
|
||||
type: number
|
||||
type: array
|
||||
samples:
|
||||
description: 每行归属的输入样品 index (0-based)
|
||||
items:
|
||||
type: integer
|
||||
type: array
|
||||
volumes:
|
||||
description: 均分后的体积列表
|
||||
items:
|
||||
type: number
|
||||
type: array
|
||||
required:
|
||||
- volumes
|
||||
- readings
|
||||
- samples
|
||||
title: SplitAndMeasure_Result
|
||||
type: object
|
||||
required:
|
||||
- goal
|
||||
title: SplitAndMeasure
|
||||
type: object
|
||||
type: UniLabJsonCommandAsync
|
||||
module: unilabos.devices.virtual.virtual_sample_demo:VirtualSampleDemo
|
||||
status_types:
|
||||
status: str
|
||||
type: python
|
||||
config_info: []
|
||||
description: Virtual sample tracking demo device
|
||||
handles: []
|
||||
icon: ''
|
||||
init_param_schema:
|
||||
config:
|
||||
properties:
|
||||
config:
|
||||
type: string
|
||||
device_id:
|
||||
type: string
|
||||
required: []
|
||||
type: object
|
||||
data:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
version: 1.0.0
|
||||
virtual_separator:
|
||||
category:
|
||||
- virtual_device
|
||||
|
||||
@@ -82,7 +82,7 @@ def get_result_info_str(error: str, suc: bool, return_value=None) -> str:
|
||||
"""
|
||||
samples = None
|
||||
if isinstance(return_value, dict):
|
||||
if "samples" in return_value:
|
||||
if "samples" in return_value and type(return_value["samples"]) in [list, tuple] and type(return_value["samples"][0]) == dict:
|
||||
samples = return_value.pop("samples")
|
||||
result_info = {"error": error, "suc": suc, "return_value": return_value, "samples": samples}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user