mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-04-25 20:59:54 +00:00
add formulation action
This commit is contained in:
@@ -1000,6 +1000,183 @@ class BioyondCellWorkstation(BioyondWorkstation):
|
|||||||
|
|
||||||
return final_result
|
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]:
|
def _extract_vial_plate_from_report(self, report: Dict) -> Optional[Dict]:
|
||||||
"""
|
"""
|
||||||
从 order_finish 报文中提取分液瓶板信息
|
从 order_finish 报文中提取分液瓶板信息
|
||||||
|
|||||||
@@ -188,6 +188,108 @@ bioyond_cell:
|
|||||||
title: create_orders参数
|
title: create_orders参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
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:
|
auto-create_sample:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
|
|||||||
@@ -632,6 +632,11 @@ class Registry:
|
|||||||
# 保留字段的 title(用户自定义的中文名)
|
# 保留字段的 title(用户自定义的中文名)
|
||||||
if "title" in prev_field and prev_field["title"]:
|
if "title" in prev_field and prev_field["title"]:
|
||||||
field_schema["title"] = 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:
|
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"]},
|
"goal_default": {i["name"]: i["default"] for i in v["args"]},
|
||||||
"handles": old_action_configs.get(f"auto-{k}", {}).get("handles", []),
|
"handles": old_action_configs.get(f"auto-{k}", {}).get("handles", []),
|
||||||
"placeholder_keys": {
|
"placeholder_keys": {
|
||||||
i["name"]: (
|
# 先用旧配置中手动定义的 placeholder_keys 作为基础
|
||||||
"unilabos_resources"
|
**old_action_configs.get(f"auto-{k}", {}).get("placeholder_keys", {}),
|
||||||
if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot"
|
# 再用自动推断的覆盖(ResourceSlot/DeviceSlot 类型)
|
||||||
or i["type"] == ("list", "unilabos.registry.placeholder_type:ResourceSlot")
|
**{
|
||||||
else "unilabos_devices"
|
i["name"]: (
|
||||||
)
|
"unilabos_resources"
|
||||||
for i in v["args"]
|
if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot"
|
||||||
if i.get("type", "")
|
or i["type"]
|
||||||
in [
|
== ("list", "unilabos.registry.placeholder_type:ResourceSlot")
|
||||||
"unilabos.registry.placeholder_type:ResourceSlot",
|
else "unilabos_devices"
|
||||||
"unilabos.registry.placeholder_type:DeviceSlot",
|
)
|
||||||
("list", "unilabos.registry.placeholder_type:ResourceSlot"),
|
for i in v["args"]
|
||||||
("list", "unilabos.registry.placeholder_type:DeviceSlot"),
|
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 {}),
|
**({"always_free": True} if v.get("always_free") else {}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"deck": {
|
"deck": {
|
||||||
"data": {
|
"data": {
|
||||||
"_resource_child_name": "YB_Bioyond_Deck",
|
"_resource_child_name": "YB_Bioyond_Deck",
|
||||||
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_YB_Deck"
|
"_resource_type": "unilabos.resources.bioyond.decks:BioyondElectrolyteDeck"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"protocol_type": [],
|
"protocol_type": [],
|
||||||
@@ -103,15 +103,14 @@
|
|||||||
"children": [],
|
"children": [],
|
||||||
"parent": "bioyond_cell_workstation",
|
"parent": "bioyond_cell_workstation",
|
||||||
"type": "deck",
|
"type": "deck",
|
||||||
"class": "BIOYOND_YB_Deck",
|
"class": "BioyondElectrolyteDeck",
|
||||||
"position": {
|
"position": {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"type": "BIOYOND_YB_Deck",
|
"type": "BioyondElectrolyteDeck",
|
||||||
"setup": true,
|
|
||||||
"rotation": {
|
"rotation": {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user