aps-agent/server/aps_domain/scheduling_dto.py

286 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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