243 lines
13 KiB
Python
243 lines
13 KiB
Python
|
|
"""康尼芜湖柔性排产 · 全流程模拟(in-process,不依赖服务端)。
|
|||
|
|
|
|||
|
|
跑通 demand/ 方案的完整链路并打印可读结果:
|
|||
|
|
0 主数据与能力池概览
|
|||
|
|
1 初始排产(瓶颈锚)→ 虚拟产线组装 + 甘特文本
|
|||
|
|
2 瓶颈产能法评估(替代台账法)
|
|||
|
|
3 三种排产模式对比(正排/倒排/瓶颈锚)
|
|||
|
|
4 交期承诺模拟(询单 → 乐观/预计完工)
|
|||
|
|
5 动态调度A:紧急插单(VIP 高优先小批量)
|
|||
|
|
6 动态调度B:设备故障(压接机停机 → 产能池收缩 → 重排)
|
|||
|
|
|
|||
|
|
运行: .venv\\Scripts\\python.exe scripts\\flex_simulation.py
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
|
|||
|
|
# 让脚本从仓库根可直接运行(把仓库根加入模块搜索路径)
|
|||
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|||
|
|
|
|||
|
|
# Windows 控制台 UTF-8(中文输出)
|
|||
|
|
try:
|
|||
|
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
from server.aps_domain.flex import capability_pools, capacity_analysis # 领域能力
|
|||
|
|
from server.engines import PoolEngine # 柔性引擎
|
|||
|
|
from server.state.seed import seed_world # 种子(含 flex*)
|
|||
|
|
from server.timeutil import add_minutes, fmt_date, today0 # 日期工具
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------- 通用工具 ----------------
|
|||
|
|
def rule(title: str) -> None:
|
|||
|
|
"""分隔标题。"""
|
|||
|
|
print("\n" + "=" * 72)
|
|||
|
|
print(f" {title}")
|
|||
|
|
print("=" * 72)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def sub(title: str) -> None:
|
|||
|
|
print(f"\n— {title} —")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def counter():
|
|||
|
|
"""独立发号器(纯内存、确定性)。"""
|
|||
|
|
c: dict[str, int] = {}
|
|||
|
|
def nid(kind: str) -> int:
|
|||
|
|
c[kind] = c.get(kind, 0) + 1
|
|||
|
|
return c[kind]
|
|||
|
|
return nid
|
|||
|
|
|
|||
|
|
|
|||
|
|
def run_flex(world, sort_mode: str, start_date: str):
|
|||
|
|
"""在给定 world 上跑一次柔性排产(清空既有产物,隔离多次运行)。"""
|
|||
|
|
for k in ("flexScheduleVersions", "flexVirtualLines", "flexWorkOrders", "flexConflicts"):
|
|||
|
|
world[k] = []
|
|||
|
|
return PoolEngine().solve(world, counter(), sort_mode=sort_mode, start_date=start_date)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def product_name(world, code: str) -> str:
|
|||
|
|
m = next((x for x in world["flexMaterials"] if x["code"] == code), None)
|
|||
|
|
return m["name"] if m else code
|
|||
|
|
|
|||
|
|
|
|||
|
|
def print_virtual_lines(world, limit_steps: bool = False) -> None:
|
|||
|
|
"""打印本版本每条虚拟产线的工序-设备-模具-时间。"""
|
|||
|
|
vls = world["flexVirtualLines"]
|
|||
|
|
wos = {(w["vlId"], w["seq"]): w for w in world["flexWorkOrders"]}
|
|||
|
|
for vl in vls:
|
|||
|
|
pname = product_name(world, vl["productCode"])
|
|||
|
|
print(f"\n ▶ {vl['vlNo']} {pname} × {vl['quantity']}套 "
|
|||
|
|
f"[{vl['plannedStart']} → {vl['plannedEnd']}] "
|
|||
|
|
f"WBS={vl.get('wbs')} 控制者={vl.get('productionController')}")
|
|||
|
|
for a in vl["assignments"]:
|
|||
|
|
w = wos.get((vl["id"], a["seq"]))
|
|||
|
|
flag = " ★瓶颈" if (w and w.get("isBottleneck")) else ""
|
|||
|
|
mold = f" 模具={a['moldCode']}" if a.get("moldCode") else ""
|
|||
|
|
extra = ""
|
|||
|
|
if w:
|
|||
|
|
extra = f"(换型{w['changeoverMin']:.0f}+移动{w['moveMin']:.0f}+加工{w['runMin']:.0f}min)"
|
|||
|
|
print(f" {a['seq']:>2} {a['operationCode']:<9} → {a['equipmentCode']:<9}"
|
|||
|
|
f"{mold:<18} {a['start']} → {a['end']} {extra}{flag}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def print_conflicts(world) -> None:
|
|||
|
|
cfs = world["flexConflicts"]
|
|||
|
|
if not cfs:
|
|||
|
|
print(" 冲突:无")
|
|||
|
|
return
|
|||
|
|
print(f" 冲突({len(cfs)}):")
|
|||
|
|
for c in cfs:
|
|||
|
|
print(f" · [{c['severity']}] {c['conflictType']} {c['description']}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================
|
|||
|
|
# 主流程
|
|||
|
|
# ============================================================
|
|||
|
|
def main() -> None:
|
|||
|
|
world = seed_world()
|
|||
|
|
start = fmt_date(add_minutes(today0(), 24 * 60)) # 明天作为排产起点
|
|||
|
|
|
|||
|
|
# ---------- 0 主数据与能力池概览 ----------
|
|||
|
|
rule("阶段 0 | 主数据与产能池(设备虚拟化 + 产能池化)")
|
|||
|
|
print(f" 布局区域:{', '.join(z['code'] + '(' + z['name'] + ')' for z in world['flexZones'])}")
|
|||
|
|
sub("产能池(工序能力 → 设备集合;★=瓶颈工序)")
|
|||
|
|
for p in capability_pools(world):
|
|||
|
|
star = "★" if p["isBottleneck"] else " "
|
|||
|
|
eqs = ", ".join(f"{e['code']}{'(可移动)' if e['movable'] else ''}" for e in p["equipment"])
|
|||
|
|
print(f" {star} {p['operationCode']:<9} {p['operationName']:<6} 设备{p['equipmentCount']}台"
|
|||
|
|
f"(可移动{p['movableCount']}) : {eqs}")
|
|||
|
|
sub("模具(工装适配 + 寿命)")
|
|||
|
|
for m in world["flexMolds"]:
|
|||
|
|
life = f"{m['lifeUsed']}/{m['lifeTotal']}"
|
|||
|
|
print(f" {m['code']:<12} {m['name']:<10} 工序{m['operationCode']:<9} "
|
|||
|
|
f"适配{','.join(m['adaptableEquipment'])} 寿命{life} 换型{m['changeoverMin']}min")
|
|||
|
|
sub("待排订单")
|
|||
|
|
for o in world["flexOrders"]:
|
|||
|
|
print(f" {o['orderNo']:<8} {product_name(world, o['productCode']):<8} ×{o['quantity']:<4} "
|
|||
|
|
f"交期{o['dueDate']} 优先级{o['priority']} 状态{o['status']} WBS={o['wbs']}")
|
|||
|
|
|
|||
|
|
# ---------- 1 初始排产(瓶颈锚) ----------
|
|||
|
|
rule("阶段 1 | 初始排产:瓶颈锚模式 → 动态组装虚拟产线")
|
|||
|
|
r = run_flex(world, "BOTTLENECK", start)
|
|||
|
|
print(f" 版本 {r['versionNo']}|模式 {r['sortMode']}|虚拟产线 {r['vlCount']} 条|"
|
|||
|
|
f"工单 {r['woCount']} 个|设备平均利用率 {r['avgUtilization']*100:.1f}%|"
|
|||
|
|
f"总延迟 {r['totalTardiness']:.1f}h")
|
|||
|
|
print(f" 识别瓶颈工序:" + ",".join(
|
|||
|
|
f"{b['operationName']}({b['operationCode']}, 池内{b['poolEquipmentCount']}台)" for b in r["bottleneck"]))
|
|||
|
|
print_virtual_lines(world)
|
|||
|
|
print()
|
|||
|
|
print_conflicts(world)
|
|||
|
|
|
|||
|
|
# ---------- 2 瓶颈产能法 ----------
|
|||
|
|
rule("阶段 2 | 产能评估:瓶颈产能法(替代台账法)")
|
|||
|
|
cap = capacity_analysis(world)
|
|||
|
|
print(f" {'工序池':<20}{'设备':>4}{'日可用(min)':>12}{'单件(min)':>10}{'日产能(件)':>10}{'本版工作量(折合日)':>16}")
|
|||
|
|
for p in cap["pools"]:
|
|||
|
|
star = "★" if p["isBottleneck"] else " "
|
|||
|
|
print(f" {star}{p['operationCode']+'/'+p['operationName']:<19}{p['equipmentCount']:>4}"
|
|||
|
|
f"{p['dailyMinutes']:>12}{p['stdTimePerUnit']:>10}{p['dailyCapacity']:>10}"
|
|||
|
|
f"{p['workDays']:>15.2f}天")
|
|||
|
|
bp = cap["bottleneckPool"]
|
|||
|
|
if bp:
|
|||
|
|
print(f"\n → 限制性瓶颈:{bp['operationName']}({bp['operationCode']}),"
|
|||
|
|
f"全厂日产能上界≈{bp['dailyCapacity']}件/天。这一道最慢,产能由它决定。")
|
|||
|
|
print(f" → 对照台账法:压接池 4 台×480min÷1min=1920件/天(虚高);瓶颈法只认最慢的焊接池。")
|
|||
|
|
|
|||
|
|
# ---------- 3 三模式对比(在压力场景下才能显出差异) ----------
|
|||
|
|
rule("阶段 3 | 排产模式对比:正排 / 倒排 / 瓶颈锚(吸收《排产逻辑》PPT)")
|
|||
|
|
print(" 为显差异,统一压缩交期至 +3~+5 天制造产能压力:")
|
|||
|
|
modes = {"ASC": "正排(EDD 最早交期优先)", "DESC": "倒排(最晚交期优先)", "BOTTLENECK": "瓶颈锚(含瓶颈单优先)"}
|
|||
|
|
print(f" {'模式':<24}{'总延迟(h)':>10}{'准时单':>8}{'最晚完工':>18}{'利用率':>8}{'冲突':>6}")
|
|||
|
|
for mode, label in modes.items():
|
|||
|
|
w2 = seed_world()
|
|||
|
|
_compress_due(w2) # 压缩交期制造压力
|
|||
|
|
rr = run_flex(w2, mode, start)
|
|||
|
|
print(f" {label:<22}{rr['totalTardiness']:>10.1f}{rr['onTimeCount']:>6}/5"
|
|||
|
|
f"{(rr['makespan'] or '—'):>19}{rr['avgUtilization']*100:>7.1f}%{rr['conflictCount']:>6}")
|
|||
|
|
print(" 说明:交期宽松时三模式结果趋同;压力下正排保最早交期、倒排后置、瓶颈锚优先喂饱瓶颈。")
|
|||
|
|
|
|||
|
|
# ---------- 4 交期承诺模拟 ----------
|
|||
|
|
rule("阶段 4 | 交期承诺模拟:询单「PDU 500套 何时能交?」")
|
|||
|
|
# 复用 world(已含在制订单)做一次 in-process 模拟
|
|||
|
|
probe_qty = 500
|
|||
|
|
# 乐观:独占资源
|
|||
|
|
w_opt = seed_world()
|
|||
|
|
w_opt["flexOrders"] = [{"id": 99, "orderNo": "PROBE", "productCode": "PDU-UNIT", "quantity": probe_qty,
|
|||
|
|
"dueDate": "2999-12-31", "priority": 1, "wbs": None,
|
|||
|
|
"productionController": None, "status": "RELEASED"}]
|
|||
|
|
run_flex(w_opt, "BOTTLENECK", start)
|
|||
|
|
opt_vl = next((v for v in w_opt["flexVirtualLines"] if v["orderNo"] == "PROBE"), None)
|
|||
|
|
# 预计:插入现有订单集竞争资源
|
|||
|
|
w_exp = seed_world()
|
|||
|
|
w_exp["flexOrders"].append({"id": 99, "orderNo": "PROBE", "productCode": "PDU-UNIT", "quantity": probe_qty,
|
|||
|
|
"dueDate": "2999-12-31", "priority": 9, "wbs": None,
|
|||
|
|
"productionController": None, "status": "RELEASED"})
|
|||
|
|
run_flex(w_exp, "BOTTLENECK", start)
|
|||
|
|
exp_vl = next((v for v in w_exp["flexVirtualLines"] if v["orderNo"] == "PROBE"), None)
|
|||
|
|
print(f" 询单:PDU配电单元 × {probe_qty} 套")
|
|||
|
|
print(f" · 乐观(独占产能) :最早 {opt_vl['plannedEnd'] if opt_vl else '—'} 完成")
|
|||
|
|
print(f" · 预计(与在制竞争):预计 {exp_vl['plannedEnd'] if exp_vl else '—'} 完成")
|
|||
|
|
print(f" · 资源缺口提示:PDU 瓶颈为激光焊接(1台);若要提前,需增焊接设备或外包焊接工序。")
|
|||
|
|
|
|||
|
|
# ---------- 5 动态调度A:紧急插单 ----------
|
|||
|
|
rule("阶段 5 | 动态调度A:紧急插单(VIP 高压线束 50套,交期紧)")
|
|||
|
|
w5 = seed_world()
|
|||
|
|
r5a = run_flex(w5, "BOTTLENECK", start) # 先有一版基线
|
|||
|
|
print(f" 插单前:{r5a['vlCount']} 条虚拟产线,最晚完工 {r5a['makespan']},总延迟 {r5a['totalTardiness']:.1f}h")
|
|||
|
|
w5["flexOrders"].append({"id": 90, "orderNo": "RUSH-001", "productCode": "HV-HARNESS", "quantity": 50,
|
|||
|
|
"dueDate": fmt_date(add_minutes(today0(), 3 * 24 * 60)), "priority": 0,
|
|||
|
|
"wbs": "WBS-HV", "productionController": "李强", "status": "RELEASED"})
|
|||
|
|
r5 = run_flex(w5, "BOTTLENECK", start) # 冻结→重排(本切片=全量重排 L4)
|
|||
|
|
rush = next((v for v in w5["flexVirtualLines"] if v["orderNo"] == "RUSH-001"), None)
|
|||
|
|
print(f" 插单后:{r5['vlCount']} 条虚拟产线,最晚完工 {r5['makespan']},"
|
|||
|
|
f"准时 {r5['onTimeCount']}/{r5['vlCount']},总延迟 {r5['totalTardiness']:.1f}h")
|
|||
|
|
if rush:
|
|||
|
|
print(f" · 插单 RUSH-001 排入:{rush['plannedStart']} → {rush['plannedEnd']}(优先占用瓶颈压接池)")
|
|||
|
|
print_conflicts(w5)
|
|||
|
|
|
|||
|
|
# ---------- 6 动态调度B:设备故障(两个对比子案例) ----------
|
|||
|
|
rule("阶段 6 | 动态调度B:设备故障 → 产能池实时收缩 → 重排")
|
|||
|
|
|
|||
|
|
sub("案例①:可移动压接机 PRESS-01 故障(压接池有冗余 → 影响小)")
|
|||
|
|
w6 = seed_world()
|
|||
|
|
_compress_due(w6) # 用压力场景才能看出延迟变化
|
|||
|
|
r6a = run_flex(w6, "BOTTLENECK", start)
|
|||
|
|
crimp_before = next(p for p in capacity_analysis(w6)["pools"] if p["operationCode"] == "OP-CRIMP")
|
|||
|
|
print(f" 故障前:压接池 {crimp_before['equipmentCount']} 台,日产能 {crimp_before['dailyCapacity']} 件,"
|
|||
|
|
f"总延迟 {r6a['totalTardiness']:.1f}h,准时 {r6a['onTimeCount']}/5")
|
|||
|
|
next(e for e in w6["flexEquipment"] if e["code"] == "PRESS-01")["status"] = "MAINTENANCE"
|
|||
|
|
r6b = run_flex(w6, "BOTTLENECK", start)
|
|||
|
|
crimp_after = next(p for p in capacity_analysis(w6)["pools"] if p["operationCode"] == "OP-CRIMP")
|
|||
|
|
print(f" 故障后:压接池 {crimp_after['equipmentCount']} 台,日产能 {crimp_after['dailyCapacity']} 件,"
|
|||
|
|
f"总延迟 {r6b['totalTardiness']:.1f}h,准时 {r6b['onTimeCount']}/5")
|
|||
|
|
print(f" · 池化韧性:自动剔除 PRESS-01 用剩余 {crimp_after['equipmentCount']} 台重排,"
|
|||
|
|
f"日产能降 {crimp_before['dailyCapacity']-crimp_after['dailyCapacity']} 件但仍有冗余,交期基本不受影响。")
|
|||
|
|
|
|||
|
|
sub("案例②:单点瓶颈激光焊接机 WELD-01 故障(无备机 → PDU 停摆)")
|
|||
|
|
w6b = seed_world()
|
|||
|
|
run_flex(w6b, "BOTTLENECK", start)
|
|||
|
|
next(e for e in w6b["flexEquipment"] if e["code"] == "WELD-01")["status"] = "MAINTENANCE"
|
|||
|
|
r6c = run_flex(w6b, "BOTTLENECK", start)
|
|||
|
|
print(f" 故障后:虚拟产线 {r6c['vlCount']} 条(PDU 单无法组线),冲突 {r6c['conflictCount']}")
|
|||
|
|
print_conflicts(w6b)
|
|||
|
|
print(" · 单点瓶颈无冗余:系统立即暴露 NO_CAPABILITY,提示应备用焊接机 / 外包焊接工序 / 议定交期。")
|
|||
|
|
|
|||
|
|
rule("模拟结束")
|
|||
|
|
print(" 全流程覆盖:设备虚拟化→产能池→虚拟产线排产→瓶颈产能评估→模式对比→交期承诺→动态重排。")
|
|||
|
|
print(" 数据口径见 docs/product/demand-data-intake.md;真实数据逐表替换 flex* 即可复跑。")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _compress_due(world) -> None:
|
|||
|
|
"""把订单交期压缩到 +3~+5 天,制造产能压力(用于模式对比/故障影响演示)。"""
|
|||
|
|
offs = [4, 3, 4, 5, 5]
|
|||
|
|
for o, off in zip(world["flexOrders"], offs):
|
|||
|
|
o["dueDate"] = fmt_date(add_minutes(today0(), off * 24 * 60))
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|