# ============================================================ # 中性排产问题/解 DTO 与 world 双向映射(moduleId: domain-scheduling-dto) # 外部算法只认 SchedulingProblem / SchedulingSolution,不碰内部 flex* 表结构 # ============================================================ from __future__ import annotations from datetime import datetime from typing import Any from pydantic import BaseModel, Field from server.timeutil import add_minutes, fmt_dt, today0 class ProblemOrder(BaseModel): orderId: str productCode: str quantity: float = 1 dueDate: str | None = None priority: int = 5 class ProblemStep(BaseModel): productCode: str seq: int operationCode: str operationName: str = "" stdTimePerUnit: float = 1.0 requireMold: bool = False class ProblemResource(BaseModel): code: str name: str = "" capabilities: list[str] = Field(default_factory=list) # operationCode 列表 zone: str | None = None class SchedulingProblem(BaseModel): schemaVersion: str = "1.1" # 契约版本(M-D) problemId: str track: str = "flex" startTime: str horizonHours: float = 168 orders: list[ProblemOrder] # schema required routings: list[ProblemStep] # schema required resources: list[ProblemResource] # schema required objectives: dict[str, float] = Field(default_factory=lambda: {"tardiness": 1.0}) constraints: dict[str, bool] = Field(default_factory=dict) meta: dict[str, Any] = Field(default_factory=dict) class SolutionOp(BaseModel): orderId: str seq: int operationCode: str operationName: str = "" resourceCode: str start: str end: str runMin: float = 0 class SolutionConflict(BaseModel): type: str = "OTHER" orderId: str | None = None description: str = "" severity: str = "medium" class SchedulingSolution(BaseModel): schemaVersion: str = "1.1" # 契约版本(M-D) problemId: str runId: str skillId: str = "" status: str = "FEASIBLE" # OPTIMAL / FEASIBLE / INFEASIBLE operations: list[SolutionOp] # schema required conflicts: list[SolutionConflict] = Field(default_factory=list) kpi: dict[str, Any] = Field(default_factory=dict) solverMeta: dict[str, Any] = Field(default_factory=dict) def world_to_flex_problem(world: dict[str, Any], *, order_ids: list[int] | None = None, start_time: str | None = None, horizon_hours: float = 168, include_knowledge: bool = False) -> SchedulingProblem: """柔性轨 world → 中性 Problem。 include_knowledge=True 时注入 meta.knowledgeRefs:按产品/工序在知识库 检索相关 SOP/工艺模式命中摘要(带出处),让外部算法拿到约束背景(M-E)。 """ orders_src = world.get("flexOrders") or [] if order_ids: idset = set(order_ids) orders_src = [o for o in orders_src if o.get("id") in idset] orders = [ ProblemOrder( orderId=str(o["orderNo"]), productCode=str(o.get("productCode") or ""), quantity=float(o.get("quantity") or 1), dueDate=o.get("dueDate"), priority=int(o.get("priority") or 5), ) for o in orders_src ] product_codes = {o.productCode for o in orders} routings = [ ProblemStep( productCode=r["productCode"], seq=int(r.get("seq") or 0), operationCode=str(r.get("operationCode") or ""), operationName=str(r.get("operationName") or r.get("operationCode") or ""), stdTimePerUnit=float(r.get("stdTimePerUnit") or 1), requireMold=bool(r.get("requireMold")), ) for r in (world.get("flexRoutings") or []) if r.get("productCode") in product_codes ] resources = [ ProblemResource( code=str(e.get("code") or ""), name=str(e.get("name") or e.get("code") or ""), capabilities=list(e.get("capabilities") or []), zone=e.get("zone"), ) for e in (world.get("flexEquipment") or []) if e.get("status", "RUNNING") in ("RUNNING", "IDLE", None) or True ] # 过滤故障设备 resources = [ r for r in resources if not any( e.get("code") == r.code and e.get("status") == "MAINTENANCE" for e in (world.get("flexEquipment") or []) ) ] start = start_time or fmt_dt(add_minutes(today0(), 24 * 60)) meta: dict[str, Any] = {"orderCount": len(orders)} if include_knowledge: meta["knowledgeRefs"] = _knowledge_refs(world, orders, routings) return SchedulingProblem( problemId=f"P-{datetime.now().strftime('%Y%m%d%H%M%S')}", track="flex", startTime=start, horizonHours=horizon_hours, orders=orders, routings=routings, resources=resources, meta=meta, ) def _knowledge_refs(world: dict[str, Any], orders: list[ProblemOrder], routings: list[ProblemStep], top_k: int = 3) -> list[dict[str, Any]]: """按产品名/工序名检索知识库,产出带出处的命中摘要(供外部算法参考)。""" try: from server.knowledge.assets import get_knowledge from server.knowledge.retrieval import hybrid_search units = get_knowledge().iter_search_units() except Exception: return [] materials = {m.get("code"): m for m in world.get("flexMaterials") or []} queries: list[str] = [] for o in orders[:5]: name = (materials.get(o.productCode) or {}).get("name") or o.productCode queries.append(f"{name} 工艺") refs: list[dict[str, Any]] = [] seen: set[str] = set() for q in queries: try: hits = hybrid_search(units, q, top_k=top_k) except Exception: continue for h in hits or []: key = f"{h.get('assetId')}#{h.get('chunkId') or ''}" if key in seen: continue seen.add(key) refs.append({ "query": q, "assetId": h.get("assetId"), "title": h.get("title"), "version": h.get("version"), "snippet": (h.get("snippet") or h.get("text") or "")[:200], "score": h.get("score"), }) return refs[:10] def apply_flex_solution(world: dict[str, Any], solution: SchedulingSolution, next_id, *, skill_id: str = "") -> dict[str, Any]: """把柔性 Solution 写入 flex* 草稿版本,返回摘要 dict(对齐 flex 排产结果字段)。""" versions = world.setdefault("flexScheduleVersions", []) vid = next_id("flexScheduleVersion") if callable(next_id) else next_id("flexScheduleVersion") version_no = f"EXT{datetime.now().strftime('%Y%m%d')}-{vid:03d}" # 按订单分组建虚拟产线 by_order: dict[str, list[SolutionOp]] = {} for op in solution.operations: by_order.setdefault(op.orderId, []).append(op) orders_by_no = {str(o["orderNo"]): o for o in (world.get("flexOrders") or [])} eq_by_code = {str(e.get("code")): e for e in (world.get("flexEquipment") or [])} vls = world.setdefault("flexVirtualLines", []) wos = world.setdefault("flexWorkOrders", []) cfs = world.setdefault("flexConflicts", []) vl_count = 0 wo_count = 0 for order_no, ops in by_order.items(): fo = orders_by_no.get(order_no) or {} vl_id = next_id("flexVirtualLine") ops_sorted = sorted(ops, key=lambda x: x.seq) start = ops_sorted[0].start if ops_sorted else None end = ops_sorted[-1].end if ops_sorted else None vls.append({ "id": vl_id, "vlNo": f"VL-{vl_id:04d}", "versionId": vid, "orderNo": order_no, "productCode": fo.get("productCode"), "quantity": fo.get("quantity") or 1, "plannedStart": start, "plannedEnd": end, "assignments": [ { "seq": op.seq, "operationCode": op.operationCode, "equipmentCode": op.resourceCode, "moldCode": None, "start": op.start, "end": op.end, } for op in ops_sorted ], }) vl_count += 1 for op in ops_sorted: eq = eq_by_code.get(op.resourceCode) or {} wo_id = next_id("flexWorkOrder") wos.append({ "id": wo_id, "orderNo": f"WO-{wo_id:04d}", "versionId": vid, "vlId": vl_id, "flexOrderNo": order_no, "productCode": fo.get("productCode"), "quantity": fo.get("quantity") or 1, "operationCode": op.operationCode, "operationName": op.operationName or op.operationCode, "seq": op.seq, "equipmentId": eq.get("id"), "equipmentCode": op.resourceCode, "equipmentName": eq.get("name") or op.resourceCode, "zone": eq.get("zone"), "moldCode": None, "teamCode": None, "changeoverMin": 0, "moveMin": 0, "runMin": op.runMin, "plannedStartTime": op.start, "plannedEndTime": op.end, "status": "PLANNED", "isBottleneck": False, }) wo_count += 1 for c in solution.conflicts: cfs.append({ "id": next_id("flexConflict"), "versionId": vid, "conflictType": c.type, "orderNo": c.orderId, "severity": c.severity, "description": c.description, "suggestedSolution": "", "isResolved": False, }) on_time = 0 for order_no, ops in by_order.items(): fo = orders_by_no.get(order_no) or {} due = fo.get("dueDate") if due and ops: end = max(op.end for op in ops) if str(end)[:10] <= str(due): on_time += 1 versions.append({ "id": vid, "versionNo": version_no, "status": "DRAFT", "sortMode": "EXTERNAL", "createdAt": fmt_dt(datetime.now()), "orderCount": len(by_order), "vlCount": vl_count, "woCount": wo_count, "conflictCount": len(solution.conflicts), "totalTardiness": float((solution.kpi or {}).get("totalTardiness") or 0), "solveStatus": solution.status, "avgUtilization": float((solution.kpi or {}).get("avgUtilization") or 0.5), "onTimeCount": on_time, "skillId": skill_id or solution.skillId, "externalRunId": solution.runId, "engineType": "EXTERNAL", }) return { "versionId": vid, "versionNo": version_no, "vlCount": vl_count, "woCount": wo_count, "conflictCount": len(solution.conflicts), "avgUtilization": float((solution.kpi or {}).get("avgUtilization") or 0.5), "onTimeCount": on_time, "sortMode": "EXTERNAL", "window": "full", "makespan": None, "bottleneck": [], "deferredCount": 0, "skillId": skill_id or solution.skillId, "runId": solution.runId, }