mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-04-24 02:29:58 +00:00
add formulation action
This commit is contained in:
@@ -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 报文中提取分液瓶板信息
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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 {}),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user