add formulation action

This commit is contained in:
Junhan Chang
2026-03-21 09:32:16 +08:00
committed by Andy6M
parent 03e3719b18
commit dff70bd72b
4 changed files with 307 additions and 18 deletions

View File

@@ -1000,6 +1000,183 @@ class BioyondCellWorkstation(BioyondWorkstation):
return final_result
def create_orders_formulation(
self,
formulation: List[Dict[str, Any]],
batch_id: str = "",
bottle_type: str = "配液小瓶",
mix_time: int = 0,
load_shedding_info: float = 0.0,
pouch_cell_info: float = 0.0,
conductivity_info: float = 0.0,
conductivity_bottle_count: int = 0,
) -> Dict[str, Any]:
"""
配方批量输入版本的 create_orders —— 等价于 create_orders
但参数来源于前端 FormulationBatchWidget而非 Excel 文件。
Args:
formulation: 配方列表,每个元素代表一个订单(一瓶),格式:
[
{
"order_name": "配方A", # 可选,配方名称
"materials": [ # 物料列表
{"name": "LiPF6", "mass": 12.5},
{"name": "EC", "mass": 50.0},
]
},
...
]
batch_id: 批次ID若为空则用当前时间戳
bottle_type: 配液瓶类型,默认 "配液小瓶"
mix_time: 混匀时间(秒)
load_shedding_info: 扣电组装分液体积
pouch_cell_info: 软包组装分液体积
conductivity_info: 电导测试分液体积
conductivity_bottle_count: 电导测试分液瓶数
Returns:
与 create_orders 返回格式一致的结果字典
"""
if not formulation:
raise ValueError("formulation 参数不能为空")
if not batch_id:
batch_id = f"formulation_{datetime.now().strftime('%Y%m%d%H%M%S')}"
create_time = f"{datetime.now().year}/{datetime.now().month}/{datetime.now().day}"
# 将 formulation 转换为 LIMS orders 格式(与 create_orders 中的格式一致)
orders: List[Dict[str, Any]] = []
for idx, item in enumerate(formulation):
materials = item.get("materials", [])
order_name = item.get("order_name", f"{batch_id}_order_{idx + 1}")
mats: List[Dict[str, Any]] = []
total_mass = 0.0
for mat in materials:
name = mat.get("name", "")
mass = float(mat.get("mass", 0.0))
if name and mass > 0:
mats.append({"name": name, "mass": mass})
total_mass += mass
if not mats:
logger.warning(f"[create_orders_formulation] 第 {idx + 1} 个配方无有效物料,跳过")
continue
orders.append({
"batchId": batch_id,
"orderName": order_name,
"createTime": create_time,
"bottleType": bottle_type,
"mixTime": mix_time,
"loadSheddingInfo": load_shedding_info,
"pouchCellInfo": pouch_cell_info,
"conductivityInfo": conductivity_info,
"conductivityBottleCount": conductivity_bottle_count,
"materialInfos": mats,
"totalMass": round(total_mass, 4),
})
if not orders:
logger.error("[create_orders_formulation] 没有有效的订单可提交")
return {"status": "error", "message": "没有有效配方数据"}
logger.info(f"[create_orders_formulation] 即将提交 {len(orders)} 个订单 (batchId={batch_id})")
# ========== 提交订单到 LIMS ==========
response = self._post_lims("/api/lims/order/orders", orders)
logger.info(f"[create_orders_formulation] 接口返回: {response}")
data_list = response.get("data", [])
if not data_list:
logger.error("创建订单未返回有效数据!")
return response
order_codes = [item.get("orderCode") for item in data_list if item.get("orderCode")]
if not order_codes:
logger.error("未找到任何有效的 orderCode")
return response
logger.info(f"[create_orders_formulation] 等待 {len(order_codes)} 个订单完成: {order_codes}")
# ========== 等待所有订单完成 ==========
all_reports = []
for idx, order_code in enumerate(order_codes, 1):
logger.info(f"[create_orders_formulation] 等待第 {idx}/{len(order_codes)} 个订单: {order_code}")
result = self.wait_for_order_finish(order_code)
if result.get("status") == "success":
all_reports.append(result.get("report", {}))
logger.info(f"[create_orders_formulation] ✓ 订单 {order_code} 完成")
else:
logger.warning(f"订单 {order_code} 状态异常: {result.get('status')}")
all_reports.append({
"orderCode": order_code,
"status": result.get("status"),
"error": result.get("message", "未知错误"),
})
# ========== 计算质量比 ==========
all_mass_ratios = []
for idx, report in enumerate(all_reports, 1):
order_code = report.get("orderCode", "N/A")
if "error" not in report:
try:
mass_ratios = self._process_order_reagents(report)
all_mass_ratios.append({
"orderCode": order_code,
"orderName": report.get("orderName", "N/A"),
"real_mass_ratio": mass_ratios.get("real_mass_ratio", {}),
"target_mass_ratio": mass_ratios.get("target_mass_ratio", {}),
})
except Exception as e:
logger.error(f"计算订单 {order_code} 质量比失败: {e}")
all_mass_ratios.append({
"orderCode": order_code,
"real_mass_ratio": {},
"target_mass_ratio": {},
"error": str(e),
})
else:
all_mass_ratios.append({
"orderCode": order_code,
"real_mass_ratio": {},
"target_mass_ratio": {},
"error": "订单未成功完成",
})
# ========== 提取分液瓶板 + 创建资源 ==========
all_vial_plates = []
processed_material_ids = set()
for report in all_reports:
vial_plate_info = self._extract_vial_plate_from_report(report)
if vial_plate_info:
material_id = vial_plate_info.get("materialId")
all_vial_plates.append(vial_plate_info)
if material_id in processed_material_ids:
continue
try:
self._create_vial_plate_resource(vial_plate_info)
processed_material_ids.add(material_id)
except Exception as e:
logger.error(f"[资源树] 创建失败: {e}")
logger.info(
f"[create_orders_formulation] 完成: "
f"{len(all_reports)} 个订单, {len(all_vial_plates)} 个分液瓶板"
)
return {
"status": "all_completed",
"total_orders": len(order_codes),
"bottle_count": len(order_codes),
"reports": all_reports,
"mass_ratios": all_mass_ratios,
"vial_plates": all_vial_plates,
"original_response": response,
}
def _extract_vial_plate_from_report(self, report: Dict) -> Optional[Dict]:
"""
从 order_finish 报文中提取分液瓶板信息

View File

@@ -188,6 +188,108 @@ bioyond_cell:
title: create_orders参数
type: object
type: UniLabJsonCommand
auto-create_orders_formulation:
always_free: true
feedback: {}
goal: {}
goal_default:
batch_id: ''
bottle_type: 配液小瓶
conductivity_bottle_count: 0
conductivity_info: 0.0
formulation: null
load_shedding_info: 0.0
mix_time: 0
pouch_cell_info: 0.0
handles:
output:
- data_key: total_orders
data_source: executor
data_type: integer
handler_key: bottle_count
label: 配液瓶数
- data_key: vial_plates
data_source: executor
data_type: array
handler_key: vial_plates_output
label: 分液瓶板列表
- data_key: mass_ratios
data_source: executor
data_type: array
handler_key: mass_ratios_output
label: 配方信息列表
placeholder_keys:
formulation: unilabos_formulation
result: {}
schema:
description: 配方批量输入版本的创建实验——通过前端配方组件输入物料配比替代Excel导入
properties:
feedback: {}
goal:
properties:
batch_id:
default: ''
description: 批次ID为空则自动生成时间戳
type: string
bottle_type:
default: 配液小瓶
description: 配液瓶类型
type: string
conductivity_bottle_count:
default: 0
description: 电导测试分液瓶数
type: integer
conductivity_info:
default: 0.0
description: 电导测试分液体积
type: number
formulation:
description: 配方列表,每个元素代表一个订单(一瓶)
items:
properties:
materials:
description: 物料列表
items:
properties:
mass:
description: 质量(g)
type: number
name:
description: 物料名称
type: string
required:
- name
- mass
type: object
type: array
order_name:
description: 配方名称(可选)
type: string
required:
- materials
type: object
type: array
load_shedding_info:
default: 0.0
description: 扣电组装分液体积
type: number
mix_time:
default: 0
description: 混匀时间(秒)
type: integer
pouch_cell_info:
default: 0.0
description: 软包组装分液体积
type: number
required:
- formulation
type: object
result: {}
required:
- goal
title: create_orders_formulation参数
type: object
type: UniLabJsonCommand
auto-create_sample:
feedback: {}
goal: {}

View File

@@ -632,6 +632,11 @@ class Registry:
# 保留字段的 title用户自定义的中文名
if "title" in prev_field and prev_field["title"]:
field_schema["title"] = prev_field["title"]
# 保留旧 schema 中手动定义的复杂嵌套结构(如 items、properties、required
# 当旧 schema 比自动生成的更丰富时,使用旧 schema 的结构
for rich_key in ("items", "properties", "required"):
if rich_key in prev_field and rich_key not in field_schema:
field_schema[rich_key] = prev_field[rich_key]
def _is_typed_dict(self, annotation: Any) -> bool:
"""
@@ -818,20 +823,26 @@ class Registry:
"goal_default": {i["name"]: i["default"] for i in v["args"]},
"handles": old_action_configs.get(f"auto-{k}", {}).get("handles", []),
"placeholder_keys": {
i["name"]: (
"unilabos_resources"
if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot"
or i["type"] == ("list", "unilabos.registry.placeholder_type:ResourceSlot")
else "unilabos_devices"
)
for i in v["args"]
if i.get("type", "")
in [
"unilabos.registry.placeholder_type:ResourceSlot",
"unilabos.registry.placeholder_type:DeviceSlot",
("list", "unilabos.registry.placeholder_type:ResourceSlot"),
("list", "unilabos.registry.placeholder_type:DeviceSlot"),
]
# 先用旧配置中手动定义的 placeholder_keys 作为基础
**old_action_configs.get(f"auto-{k}", {}).get("placeholder_keys", {}),
# 再用自动推断的覆盖ResourceSlot/DeviceSlot 类型)
**{
i["name"]: (
"unilabos_resources"
if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot"
or i["type"]
== ("list", "unilabos.registry.placeholder_type:ResourceSlot")
else "unilabos_devices"
)
for i in v["args"]
if i.get("type", "")
in [
"unilabos.registry.placeholder_type:ResourceSlot",
"unilabos.registry.placeholder_type:DeviceSlot",
("list", "unilabos.registry.placeholder_type:ResourceSlot"),
("list", "unilabos.registry.placeholder_type:DeviceSlot"),
]
},
},
**({"always_free": True} if v.get("always_free") else {}),
}

View File

@@ -13,7 +13,7 @@
"deck": {
"data": {
"_resource_child_name": "YB_Bioyond_Deck",
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_YB_Deck"
"_resource_type": "unilabos.resources.bioyond.decks:BioyondElectrolyteDeck"
}
},
"protocol_type": [],
@@ -103,15 +103,14 @@
"children": [],
"parent": "bioyond_cell_workstation",
"type": "deck",
"class": "BIOYOND_YB_Deck",
"class": "BioyondElectrolyteDeck",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "BIOYOND_YB_Deck",
"setup": true,
"type": "BioyondElectrolyteDeck",
"rotation": {
"x": 0,
"y": 0,