aps-agent/server/aps_domain/scheduling_dto.py

286 lines
11 KiB
Python
Raw Normal View History

# ============================================================
# 中性排产问题/解 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,
}