"""康尼芜湖柔性排产 · 全流程模拟(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()