# 智能 RGV 的动态调度策略（cumcm2018b）出图脚本
# 数据唯一真源 = gen_data.gen_2018b
# 生成 cumcm2018b-{1,2,3}-fig{1..8}.svg 共 24 张，图/正文/附录/工具数字一致。
import os, math, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import gen_data as GD

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(ROOT, "assets", "problems", "papers")
os.makedirs(OUT, exist_ok=True)

STRATS = GD.RGV_STRATS
ST_INFO = [("cyclic", "正则循环", "#2563eb"), ("greedy", "贪心反应", "#0d9488"),
           ("lookahead", "前瞻均衡", "#d97706"), ("rand", "随机基线", "#dc2626")]
GC = ["#2563eb", "#0d9488", "#dc2626"]  # 三组配色
GL = ["第1组", "第2组", "第3组"]


def esc(s):
    return str(s).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")


def _lerp(c1, c2, t):
    return tuple(int(c1[k] + (c2[k] - c1[k]) * t) for k in range(3))


def _hex(c):
    return "#%02x%02x%02x" % c


def frame(title, body, W=780, H=470):
    return ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d">'
            '<rect width="100%%" height="100%%" fill="#ffffff"/>'
            '<text x="%d" y="34" font-size="17" font-weight="700" fill="#0f172a">%s</text>'
            '%s</svg>') % (W, H, W, H, max(20, W // 2 - len(title) * 4), esc(title), body)


def bar(title, labels, vals, color="#2563eb", ylabel="", xlabel="", fmt="{:.2f}", W=780, H=470):
    vals = [float(v) for v in vals]; n = len(vals)
    vmax = max(vals); vmin = min(0.0, min(vals))
    ymax = vmax * 1.15 if vmax > 0 else 1.0
    ymin = vmin * 1.15 if vmin < 0 else 0.0
    y0, x0, bw, bh = 60, 95, W - 165, H - 150
    def Y(v): return y0 + bh - (v - ymin) / (ymax - ymin or 1) * bh
    body = '<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#cbd5e1"/>' % (x0, y0 + bh, x0 + bw, y0 + bh)
    body += '<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#cbd5e1"/>' % (x0, y0, x0, y0 + bh)
    zero_y = Y(0)
    body += '<line x1="%d" y1="%.1f" x2="%d" y2="%.1f" stroke="#94a3b8" stroke-dasharray="3 2"/>' % (x0, zero_y, x0 + bw, zero_y)
    bw_bar = bw / n * 0.55
    for i, v in enumerate(vals):
        cx = x0 + (i + 0.5) * bw / n
        top = Y(max(v, 0)); bot = Y(min(v, 0))
        body += '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="%s" fill-opacity="0.9"/>' % (cx - bw_bar / 2, top, bw_bar, max(bot - top, 0.5), color)
        body += '<text x="%.1f" y="%.1f" font-size="10" fill="#334155" text-anchor="middle">%s</text>' % (cx, top - 6, esc(fmt.format(v)))
        body += '<text x="%.1f" y="%d" font-size="11" fill="#475569" text-anchor="middle">%s</text>' % (cx, y0 + bh + 18, esc(labels[i]))
    if ylabel:
        body += '<text x="%d" y="%d" font-size="11" fill="#6b7280" transform="rotate(-90 %d %d)" text-anchor="middle">%s</text>' % (x0 - 64, y0 + bh / 2, x0 - 64, y0 + bh / 2, esc(ylabel))
    if xlabel:
        body += '<text x="%d" y="%d" font-size="11" fill="#6b7280">%s</text>' % (x0 + bw / 2, y0 + bh + 36, esc(xlabel))
    return frame(title, body, W, H)


def line(title, series, xlabel="", ylabel="", W=780, H=470, vlines=()):
    pts_all = [p for s in series for p in s[2]]
    xs = [p[0] for p in pts_all]; ys = [p[1] for p in pts_all]
    xmin, xmax = min(xs), max(xs); ymin, ymax = min(ys), max(ys)
    padx = (xmax - xmin) * 0.08 or 1; pady = (ymax - ymin) * 0.08 or 1
    xmin -= padx; xmax += padx; ymin -= pady; ymax += pady
    x0, y0, bw, bh = 90, 60, W - 150, H - 140
    def X(v): return x0 + (v - xmin) / (xmax - xmin or 1) * bw
    def Y(v): return y0 + bh - (v - ymin) / (ymax - ymin or 1) * bh
    body = '<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#cbd5e1"/>' % (x0, y0 + bh, x0 + bw, y0 + bh)
    body += '<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#cbd5e1"/>' % (x0, y0, x0, y0 + bh)
    for xv in vlines:
        body += '<line x1="%.1f" y1="%d" x2="%.1f" y2="%d" stroke="#dc2626" stroke-dasharray="4 3"/>' % (X(xv), y0, X(xv), y0 + bh)
    for lab, col, pts in series:
        if len(pts) > 1:
            d = "".join('M%.1f,%.1f L%.1f,%.1f ' % (X(pts[i][0]), Y(pts[i][1]), X(pts[i + 1][0]), Y(pts[i + 1][1])) for i in range(len(pts) - 1))
            body += '<path d="%s" fill="none" stroke="%s" stroke-width="2.2"/>' % (d, col)
        for x, y in pts:
            body += '<circle cx="%.1f" cy="%.1f" r="2.6" fill="%s"/>' % (X(x), Y(y), col)
        if lab:
            body += '<text x="%d" y="%d" font-size="11" fill="%s">%s</text>' % (x0 + bw - 150, y0 + 18 + series.index((lab, col, pts)) * 16, col, esc(lab))
    if xlabel: body += '<text x="%d" y="%d" font-size="11" fill="#6b7280">%s</text>' % (x0 + bw / 2, y0 + bh + 34, esc(xlabel))
    if ylabel: body += '<text x="%d" y="%d" font-size="11" fill="#6b7280" transform="rotate(-90 %d %d)" text-anchor="middle">%s</text>' % (x0 - 60, y0 + bh / 2, x0 - 60, y0 + bh / 2, esc(ylabel))
    return frame(title, body, W, H)


def grouped_bar(title, labels, groups, W=780, H=470, ylabel=""):
    n = len(labels); ng = len(groups)
    ymax = max(max(g[2]) for g in groups) * 1.15 or 1
    y0, x0, bw, bh = 60, 95, W - 165, H - 150
    def Y(v): return y0 + bh - v / ymax * bh
    body = '<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#cbd5e1"/>' % (x0, y0 + bh, x0 + bw, y0 + bh)
    body += '<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#cbd5e1"/>' % (x0, y0, x0, y0 + bh)
    groupw = bw / n * 0.8; barw = groupw / ng
    for i, lab in enumerate(labels):
        for gi, (name, color, vals) in enumerate(groups):
            v = vals[i]
            cx = x0 + (i + 0.5) * bw / n - groupw / 2 + (gi + 0.5) * barw
            body += '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="%s" fill-opacity="0.9"/>' % (cx - barw / 2, Y(v), barw, Y(0) - Y(v), color)
            body += '<text x="%.1f" y="%.1f" font-size="8" fill="#334155" text-anchor="middle">%.1f</text>' % (cx, Y(v) - 3, v)
        body += '<text x="%.1f" y="%d" font-size="11" fill="#475569" text-anchor="middle">%s</text>' % (x0 + (i + 0.5) * bw / n, y0 + bh + 18, esc(lab))
    ly = y0 + 14
    for name, color, vals in groups:
        body += '<rect x="%d" y="%d" width="10" height="10" fill="%s"/>' % (x0 + bw - 110, ly, color)
        body += '<text x="%d" y="%d" font-size="11" fill="#334155">%s</text>' % (x0 + bw - 96, ly + 9, esc(name))
        ly += 18
    if ylabel:
        body += '<text x="%d" y="%d" font-size="11" fill="#6b7280" transform="rotate(-90 %d %d)" text-anchor="middle">%s</text>' % (x0 - 64, y0 + bh / 2, x0 - 64, y0 + bh / 2, esc(ylabel))
    return frame(title, body, W, H)


def radar(title, labels, polys, W=780, H=470):
    cx, cy = W / 2, H / 2 + 8
    Rr = min(W, H) / 2 - 70
    n = len(labels)
    def P(axis, val):
        ang = -math.pi / 2 + axis * 2 * math.pi / n
        return (cx + Rr * val * math.cos(ang), cy + Rr * val * math.sin(ang))
    body = ""
    for ring in [0.25, 0.5, 0.75, 1.0]:
        pts = " ".join("%.1f,%.1f" % P(i, ring) for i in range(n))
        body += '<polygon points="%s" fill="none" stroke="#e2e8f0"/>' % pts
    for i in range(n):
        x, y = P(i, 1.0)
        body += '<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="#e2e8f0"/>' % (cx, cy, x, y)
        lx, ly = P(i, 1.18)
        body += '<text x="%.1f" y="%.1f" font-size="10" fill="#334155" text-anchor="middle">%s</text>' % (lx, ly + 4, esc(labels[i]))
    for name, color, vals in polys:
        pts = " ".join("%.1f,%.1f" % P(i, max(0.0, min(1.0, vals[i]))) for i in range(n))
        body += '<polygon points="%s" fill="%s" fill-opacity="0.22" stroke="%s" stroke-width="2"/>' % (pts, color, color)
        for i in range(n):
            x, y = P(i, max(0.0, min(1.0, vals[i])))
            body += '<circle cx="%.1f" cy="%.1f" r="2.5" fill="%s"/>' % (x, y, color)
    ly = 50
    for name, color, vals in polys:
        body += '<rect x="%d" y="%d" width="10" height="10" fill="%s"/>' % (W - 200, ly, color)
        body += '<text x="%d" y="%d" font-size="11" fill="#334155">%s</text>' % (W - 186, ly + 9, esc(name))
        ly += 20
    return frame(title, body, W, H)


def save(name, svg):
    open(os.path.join(OUT, name + ".svg"), "w").write(svg)


# ===================== 自定义图元 =====================
def hbar(title, segs, W=780, H=360):
    """水平堆叠条：segs=[(label,val,color)]，val 为占比（已×100）。"""
    x0, y0, bw = 110, 150, W - 200
    body = ""
    cx = x0
    for (lab, val, col) in segs:
        w = bw * val / 100.0
        body += '<rect x="%.1f" y="%d" width="%.1f" height="34" fill="%s"/>' % (cx, y0, w, col)
        if w > 42:
            body += '<text x="%.1f" y="%d" font-size="12" fill="#ffffff" text-anchor="middle">%.1f%%</text>' % (cx + w / 2, y0 + 22, val)
        body += '<text x="%d" y="%d" font-size="12" fill="#334155" text-anchor="end">%s</text>' % (x0 - 10, y0 + 23, esc(lab))
        cx += w
    body += '<text x="%d" y="%d" font-size="11" fill="#6b7280">RGV 单班次时间构成（移动 + 上下料 + 清洗 + 等待 = 100%%）</text>' % (x0, y0 - 16)
    return frame(title, body, W, H)


def schematic(title, W=780, H=470):
    body = ""
    # 顶部传送带（原料入）
    body += '<rect x="80" y="70" width="620" height="22" fill="#e0f2fe" stroke="#0ea5e9"/>'
    body += '<text x="390" y="64" font-size="11" fill="#0369a1" text-anchor="middle">原料传送带（上料）</text>'
    # 底部传送带（成品出）
    body += '<rect x="80" y="360" width="620" height="22" fill="#dcfce7" stroke="#16a34a"/>'
    body += '<text x="390" y="402" font-size="11" fill="#15803d" text-anchor="middle">成品传送带（下料）</text>'
    # 8 台 CNC
    cw = 60; gap = (620 - 8 * cw) / 7
    for i in range(8):
        x = 80 + i * (cw + gap)
        col = "#0d9488" if i % 2 == 0 else "#d97706"  # 奇=s1 偶=s2
        body += '<rect x="%.1f" y="175" width="%d" height="80" rx="6" fill="%s" fill-opacity="0.85"/>' % (x, cw, col)
        body += '<text x="%.1f" y="%d" font-size="12" fill="#fff" text-anchor="middle" font-weight="700">CNC%%d</text>' % (x + cw / 2, 215)
        body += '<text x="%.1f" y="%d" font-size="10" fill="#fff" text-anchor="middle">%s</text>' % (x + cw / 2, 233, esc("第1道" if i % 2 == 0 else "第2道"))
    # RGV 轨道
    body += '<line x1="80" y1="300" x2="700" y2="300" stroke="#1e293b" stroke-width="3"/>'
    body += '<rect x="300" y="288" width="80" height="24" rx="5" fill="#dc2626"/>'
    body += '<text x="340" y="304" font-size="12" fill="#fff" text-anchor="middle" font-weight="700">RGV</text>'
    body += '<text x="390" y="330" font-size="11" fill="#475569" text-anchor="middle">轨道（RGV 沿直线往返移动）</text>'
    # 清洗槽
    body += '<rect x="690" y="270" width="50" height="60" rx="6" fill="#7c3aed" fill-opacity="0.85"/>'
    body += '<text x="715" y="%d" font-size="10" fill="#fff" text-anchor="middle">清洗</text>' % 300
    body += '<text x="715" y="314" font-size="9" fill="#fff" text-anchor="middle">槽</text>'
    # 物料流箭头
    body += '<text x="390" y="150" font-size="11" fill="#475569" text-anchor="middle">物料流：原料→奇号CNC(第1道)→偶号CNC(第2道)→清洗→成品</text>'
    return frame(title, body, W, H)


def fsm(title, W=780, H=470):
    body = ""
    # 状态框
    states = [("raw", 90, 90), ("semi", 340, 90), ("final", 590, 90), ("None", 340, 300)]
    sc = {"raw": "#2563eb", "semi": "#d97706", "final": "#dc2626", "None": "#94a3b8"}
    for (s, x, y) in states:
        body += '<rect x="%d" y="%d" width="110" height="44" rx="8" fill="%s" fill-opacity="0.9"/>' % (x, y, sc[s])
        body += '<text x="%d" y="%d" font-size="13" fill="#fff" text-anchor="middle" font-weight="700">carry=%%s</text>' % (x + 55, y + 28)
    # 转移
    body += '<line x1="200" y1="112" x2="340" y2="112" stroke="#334155" stroke-width="2" marker-end="url(#ar)"/>'
    body += '<line x1="450" y1="112" x2="590" y2="112" stroke="#334155" stroke-width="2" marker-end="url(#ar)"/>'
    body += '<line x1="395" y1="134" x2="395" y2="300" stroke="#334155" stroke-width="2" marker-end="url(#ar)"/>'
    body += '<text x="265" y="104" font-size="10" fill="#475569" text-anchor="middle">第1道装料</text>'
    body += '<text x="515" y="104" font-size="10" fill="#475569" text-anchor="middle">第2道装料</text>'
    body += '<text x="408" y="225" font-size="10" fill="#475569" text-anchor="middle">清洗交付</text>'
    # 策略决策框
    body += '<rect x="120" y="380" width="540" height="64" rx="8" fill="#f1f5f9" stroke="#475569"/>'
    body += '<text x="130" y="402" font-size="11" fill="#0f172a" font-weight="700">服务目标选择（依 carry 决定角色集 rs）：</text>'
    body += '<text x="130" y="422" font-size="10" fill="#475569">正则循环(cyclic)：固定顺序轮询，遇未就绪则等待</text>'
    body += '<text x="130" y="438" font-size="10" fill="#475569">贪心(greedy)：选服务开始时刻最早者；前瞻(lookahead)：瓶颈感知+负载均衡；随机(rand)：可行者中随机</text>'
    body += '<defs><marker id="ar" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto"><path d="M0,0 L9,4.5 L0,9 z" fill="#334155"/></marker></defs>'
    return frame(title, body, W, H)


def gantt(title, trace_rgv, trace_cnc, tmax=600, W=780, H=470):
    x0, x1, y0 = 110, W - 60, 70
    def X(t): return x0 + (t / tmax) * (x1 - x0)
    body = '<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#cbd5e1"/>' % (x0, y0, x1, y0)
    # RGV 活动条
    rgv_y = 84; rh = 20
    body += '<text x="%d" y="%d" font-size="11" fill="#334155" text-anchor="end">RGV</text>' % (x0 - 8, rgv_y + 14)
    if trace_rgv and len(trace_rgv) > 1:
        for k in range(len(trace_rgv) - 1):
            t = trace_rgv[k][0]; tn = trace_rgv[k + 1][0]
            act = trace_rgv[k][3]
            if tn > tmax: break
            if "deliver" in act: col = "#dc2626"
            elif "unload" in act: col = "#d97706"
            elif "load" in act: col = "#0d9488"
            else: col = "#2563eb"
            body += '<rect x="%.1f" y="%d" width="%.1f" height="%d" fill="%s" fill-opacity="0.85"/>' % (X(t), rgv_y, max(1.0, X(tn) - X(t)), rh, col)
    # CNC 行
    cnc_y0 = 120; rowh = 36
    cmap = {"s1": "#0d9488", "s2": "#d97706", "proc": "#2563eb"}
    for c in range(8):
        yy = cnc_y0 + c * rowh
        body += '<text x="%d" y="%d" font-size="10" fill="#334155" text-anchor="end">CNC%%d</text>' % (x0 - 8, yy + 22)
        body += '<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#eef2f7"/>' % (x0, yy + rowh - 4, x1, yy + rowh - 4)
    if trace_cnc:
        for (cid, st, en, role) in trace_cnc:
            if st > tmax: continue
            yy = cnc_y0 + (cid - 1) * rowh
            body += '<rect x="%.1f" y="%d" width="%.1f" height="%d" fill="%s" fill-opacity="0.85"/>' % (X(st), yy + 6, max(1.0, X(min(en, tmax)) - X(st)), rowh - 14, cmap.get(role, "#64748b"))
    # 时间轴刻度
    for k in range(0, tmax + 1, 120):
        body += '<text x="%.1f" y="%d" font-size="9" fill="#94a3b8" text-anchor="middle">%ds</text>' % (X(k), cnc_y0 + 8 * rowh + 4, k)
    # 图例
    lx = x0 + 20
    for (lab, col) in [("装料", "#0d9488"), ("卸料", "#d97706"), ("清洗", "#dc2626"), ("移动", "#2563eb")]:
        body += '<rect x="%d" y="%d" width="10" height="10" fill="%s"/>' % (lx, cnc_y0 + 8 * rowh + 16, col)
        body += '<text x="%d" y="%d" font-size="10" fill="#334155">%s</text>' % (lx + 14, cnc_y0 + 8 * rowh + 25, esc(lab))
        lx += 70
    body += '<text x="%d" y="%d" font-size="11" fill="#6b7280">前 %d 秒调度快照（两道工序·第1组·前瞻均衡策略）</text>' % (x0, 56, tmax)
    return frame(title, body, W, H)


def box(title, labels, data, W=780, H=470):
    n = len(labels); ymax = max(max(d) for d in data) * 1.05
    ymin = min(min(d) for d in data) * 0.95
    y0, x0, bw, bh = 60, 90, W - 150, H - 150
    def Y(v): return y0 + bh - (v - ymin) / (ymax - ymin or 1) * bh
    body = '<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#cbd5e1"/>' % (x0, y0 + bh, x0 + bw, y0 + bh)
    body += '<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#cbd5e1"/>' % (x0, y0, x0, y0 + bh)
    bw_bar = bw / n * 0.5
    for i, lab in enumerate(labels):
        s = sorted(data[i]); m = len(s)
        q1 = s[int(0.25 * (m - 1))]; q2 = s[int(0.5 * (m - 1))]; q3 = s[int(0.75 * (m - 1))]
        mn = s[0]; mx = s[-1]; mu = sum(s) / m
        cx = x0 + (i + 0.5) * bw / n
        col = ST_INFO[i][2] if i < 4 else "#64748b"
        body += '<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s"/>' % (cx, Y(mn), cx, Y(q1), col)
        body += '<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s"/>' % (cx, Y(q3), cx, Y(mx), col)
        body += '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="%s" fill-opacity="0.3" stroke="%s"/>' % (cx - bw_bar / 2, Y(q3), bw_bar, Y(q1) - Y(q3), col, col)
        body += '<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="2"/>' % (cx - bw_bar / 2, Y(q2), cx + bw_bar / 2, Y(q2), col)
        body += '<circle cx="%.1f" cy="%.1f" r="2.5" fill="%s"/>' % (cx, Y(mu), "#0f172a")
        body += '<text x="%.1f" y="%d" font-size="11" fill="#475569" text-anchor="middle">%s</text>' % (cx, y0 + bh + 18, esc(lab))
        body += '<text x="%.1f" y="%.1f" font-size="9" fill="#334155" text-anchor="middle">μ=%.1f</text>' % (cx, Y(mx) - 4, mu)
    body += '<text x="%d" y="%d" font-size="11" fill="#6b7280" transform="rotate(-90 %d %d)" text-anchor="middle">单班次成品数（120 次 MC）</text>' % (x0 - 60, y0 + bh / 2, x0 - 60, y0 + bh / 2)
    return frame(title, body, W, H)


def hist(title, samples, nb=12, color="#2563eb", W=780, H=470):
    lo, hi = min(samples), max(samples)
    if hi == lo:
        hi = lo + 1.0          # 退化分布（全部相同值）时给一个单位宽度
    edges = [lo + (hi - lo) * k / nb for k in range(nb + 1)]
    cnt = [0] * nb
    for v in samples:
        b = min(nb - 1, int((v - lo) / (hi - lo) * nb))
        cnt[b] += 1
    cmax = max(cnt) or 1
    y0, x0, bw, bh = 60, 90, W - 150, H - 150
    def X(v): return x0 + (v - lo) / (hi - lo or 1) * bw
    def Y(c): return y0 + bh - c / cmax * bh
    body = '<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#cbd5e1"/>' % (x0, y0 + bh, x0 + bw, y0 + bh)
    body += '<line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#cbd5e1"/>' % (x0, y0, x0, y0 + bh)
    for b in range(nb):
        x = X(edges[b]); w = X(edges[b + 1]) - X(edges[b])
        body += '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="%s" fill-opacity="0.85"/>' % (x, Y(cnt[b]), w - 1, y0 + bh - Y(cnt[b]), color)
    mu = sum(samples) / len(samples)
    body += '<line x1="%.1f" y1="%d" x2="%.1f" y2="%d" stroke="#dc2626" stroke-width="2" stroke-dasharray="4 3"/>' % (X(mu), y0, X(mu), y0 + bh)
    body += '<text x="%d" y="%d" font-size="11" fill="#6b7280" transform="rotate(-90 %d %d)" text-anchor="middle">频数</text>' % (x0 - 60, y0 + bh / 2, x0 - 60, y0 + bh / 2)
    body += '<text x="%d" y="%d" font-size="11" fill="#6b7280">单班次成品数（红虚线=均值 %.1f）</text>' % (x0, y0 + bh + 34, mu)
    return frame(title, body, W, H)


def heat(title, row_labels, col_labels, vals, W=780, H=470):
    n = len(row_labels); m = len(col_labels)
    x0, y0, bw, bh = 160, 90, 420, 300
    cw = bw / m; ch = bh / n
    vmax = max(max(r) for r in vals) or 1
    gray = (241, 245, 249); hi = (37, 99, 235)
    body = ""
    for i in range(n):
        for j in range(m):
            v = vals[i][j]; t = v / vmax
            col = _lerp(gray, hi, t)
            body += '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" fill="%s"/>' % (x0 + j * cw, y0 + i * ch, cw + 0.5, ch + 0.5, _hex(col))
            body += '<text x="%.1f" y="%.1f" font-size="15" font-weight="700" fill="#0f172a" text-anchor="middle">%.1f</text>' % (x0 + j * cw + cw / 2, y0 + i * ch + ch / 2 + 5, v)
    for j in range(m):
        body += '<text x="%.1f" y="%d" font-size="12" fill="#475569" text-anchor="middle">%s</text>' % (x0 + j * cw + cw / 2, y0 + bh + 24, esc(col_labels[j]))
    for i in range(n):
        body += '<text x="%d" y="%.1f" font-size="12" fill="#475569" text-anchor="middle">%s</text>' % (x0 - 26, y0 + i * ch + ch / 2 + 5, esc(row_labels[i]))
    body += '<text x="%d" y="%d" font-size="11" fill="#6b7280">单元=该组该策略单班次成品数 MC 均值（颜色越深越高）</text>' % (x0, y0 - 16)
    return frame(title, body, W, H)


# ===================== 计算（唯一真源） =====================
D = GD.gen_2018b()

# ===================== 论文1：建模与离散事件仿真验证 =====================
def paper1():
    # fig1 系统布局示意图
    save("cumcm2018b-1-fig1", schematic("图1 智能 RGV 调度系统物理布局与物料流"))
    # fig2 RGV 状态机与策略决策
    save("cumcm2018b-1-fig2", fsm("图2 RGV 状态机（carry）与四策略服务目标选择逻辑"))
    # fig3 单工序 四策略×三组
    save("cumcm2018b-1-fig3", grouped_bar(
        "图3 单工序场景：四策略 × 三组 单班次成品数", GL,
        [(nm, col, [D["thru_single"][0][st], D["thru_single"][1][st], D["thru_single"][2][st]]) for (st, nm, col) in ST_INFO],
        ylabel="成品数 / 班"))
    # fig4 两道工序 四策略×三组
    save("cumcm2018b-1-fig4", grouped_bar(
        "图4 两道工序场景：四策略 × 三组 单班次成品数", GL,
        [(nm, col, [D["thru_two"][0][st], D["thru_two"][1][st], D["thru_two"][2][st]]) for (st, nm, col) in ST_INFO],
        ylabel="成品数 / 班"))
    # fig5 RGV 时间分解（代表运行）
    bd = D["bd_rep_pct"]
    segs = [("移动", bd["move"], "#2563eb"), ("上下料", bd["load"], "#0d9488"),
            ("清洗", bd["deliver"], "#dc2626"), ("等待", bd["wait"], "#94a3b8")]
    save("cumcm2018b-1-fig5", hbar("图5 代表运行 RGV 单班次时间构成（两道·第1组·前瞻均衡）", segs))
    # fig6 8 台 CNC 占用率
    save("cumcm2018b-1-fig6", bar(
        "图6 8 台 CNC 单机占用率（两道·第1组·前瞻均衡）",
        ["CNC%d" % (i + 1) for i in range(8)], [round(u * 100, 1) for u in D["util_rep"]],
        color="#7c3aed", ylabel="占用率 %", fmt="{:.1f}"))
    # fig7 累计产出曲线
    save("cumcm2018b-1-fig7", line(
        "图7 累计产出曲线（两道·第1组·前瞻均衡，按 30 分钟网格）",
        [("累计成品数", "#2563eb", D["cum_rep"])], xlabel="时间（小时）", ylabel="累计成品数"))
    # fig8 Gantt 快照
    save("cumcm2018b-1-fig8", gantt("图8 调度 Gantt 快照（前 600 秒）", D["trace_rep"], D["trace_cnc_rep"], tmax=600))


# ===================== 论文2：确定性调度优化 =====================
def paper2():
    # fig1 移动时间弹性
    save("cumcm2018b-2-fig1", line(
        "图1 移动时间缩放（0.8/1.0/1.2×）对两道吞吐量的影响（第1组·前瞻均衡）",
        [("两道成品数", "#2563eb", D["sens_move"])], xlabel="移动时间缩放系数", ylabel="成品数 / 班"))
    # fig2 上下料弹性
    save("cumcm2018b-2-fig2", line(
        "图2 上下料时间缩放（0.8/1.0/1.2×）对两道吞吐量的影响（第1组·前瞻均衡）",
        [("两道成品数", "#0d9488", D["sens_lu"])], xlabel="上下料时间缩放系数", ylabel="成品数 / 班"))
    # fig3 加工弹性
    save("cumcm2018b-2-fig3", line(
        "图3 加工时间缩放（0.8/1.0/1.2×）对两道吞吐量的影响（第1组·前瞻均衡）",
        [("两道成品数", "#d97706", D["sens_P"])], xlabel="加工时间缩放系数", ylabel="成品数 / 班"))
    # fig4 三杠杆弹性对比
    save("cumcm2018b-2-fig4", grouped_bar(
        "图4 三类时间杠杆的吞吐量弹性对比（第1组·前瞻均衡）",
        ["0.8×", "1.0×", "1.2×"],
        [("移动时间", "#2563eb", [v for (_, v) in D["sens_move"]]),
         ("上下料时间", "#0d9488", [v for (_, v) in D["sens_lu"]]),
         ("加工时间", "#d97706", [v for (_, v) in D["sens_P"]])], ylabel="成品数 / 班"))
    # fig5 吞吐量 vs CNC 数
    save("cumcm2018b-2-fig5", line(
        "图5 单工序吞吐量随 CNC 数量的变化（第1组·前瞻均衡）",
        [("成品数", "#2563eb", D["thru_vs_ncnc"])], xlabel="启用 CNC 数", ylabel="成品数 / 班"))
    # fig6 角色分配
    save("cumcm2018b-2-fig6", line(
        "图6 两道工序角色分配：s1（第1道）台数 → 产量（第1组·前瞻均衡）",
        [("成品数", "#7c3aed", D["assign"])], xlabel="s1 台数（其余为 s2）", ylabel="成品数 / 班"))
    # fig7 两道场景 三组 × 三策略
    sub = [s for s in ST_INFO if s[0] in ("cyclic", "greedy", "lookahead")]
    save("cumcm2018b-2-fig7", grouped_bar(
        "图7 两道场景：三组 × {正则循环, 贪心反应, 前瞻均衡}（注意第2组贪心逆转）", GL,
        [(nm, col, [D["thru_two"][0][st], D["thru_two"][1][st], D["thru_two"][2][st]]) for (st, nm, col) in sub],
        ylabel="成品数 / 班"))
    # fig8 最优策略与随机基线差距
    best = []; rnd = []
    for gi in range(3):
        b = max(D["thru_two"][gi][s] for s in ("cyclic", "greedy", "lookahead"))
        best.append(b); rnd.append(D["thru_two"][gi]["rand"])
    save("cumcm2018b-2-fig8", grouped_bar(
        "图8 各组的“最优策略”成品数与随机基线的差距（调度价值）", GL,
        [("最优策略", "#2563eb", best), ("随机基线", "#dc2626", rnd)], ylabel="成品数 / 班"))


# ===================== 论文3：不确定性量化与稳健设计 =====================
def paper3():
    # fig1 MC 均值 三组×四策略
    save("cumcm2018b-3-fig1", grouped_bar(
        "图1 不确定性下（故障0.01+波动0.1）单班次成品数 MC 均值：三组 × 四策略", GL,
        [(nm, col, [D["mc"][(0, st)]["mean"], D["mc"][(1, st)]["mean"], D["mc"][(2, st)]["mean"]]) for (st, nm, col) in ST_INFO],
        ylabel="成品数 / 班（均值）"))
    # fig2 箱线（第1组 四策略）
    data = [D["mc"][(0, st)]["samples"] for (st, _, _) in ST_INFO]
    save("cumcm2018b-3-fig2", box("图2 第1组四策略产量分布箱线图（120 次 MC，■=均值）",
                                  [nm for (_, nm, _) in ST_INFO], data))
    # fig3 故障 vs 波动 单独影响（第1组）
    save("cumcm2018b-3-fig3", grouped_bar(
        "图3 不确定来源分解（第1组）：仅故障 / 仅波动 / 二者叠加 的 MC 均值",
        [nm for (_, nm, _) in ST_INFO],
        [("仅故障(0.01)", "#dc2626", [D["mc_fail"][(0, st)]["mean"] for (st, _, _) in ST_INFO]),
         ("仅波动(0.1)", "#d97706", [D["mc_var"][(0, st)]["mean"] for (st, _, _) in ST_INFO]),
         ("二者叠加", "#2563eb", [D["mc"][(0, st)]["mean"] for (st, _, _) in ST_INFO])], ylabel="成品数 / 班"))
    # fig4 故障率敏感性
    fs = D["fail_sens"]
    save("cumcm2018b-3-fig4", line(
        "图4 故障率敏感性（两道·第1组）：贪心反应 vs 前瞻均衡",
        [("贪心反应", "#0d9488", fs["greedy"]), ("前瞻均衡", "#d97706", fs["lookahead"])],
        xlabel="单件故障率", ylabel="成品数 / 班（均值，80 次）"))
    # fig5 产量直方图（第1组 前瞻均衡）
    save("cumcm2018b-3-fig5", hist("图5 第1组前瞻均衡策略单班次产量直方图（120 次 MC）",
                                   D["mc"][(0, "lookahead")]["samples"], nb=12, color="#d97706"))
    # fig6 稳健性雷达（第1组）
    base = D["mc"][(0, "lookahead")]
    axes_lab = ["MC均值", "下限p5", "上限p95", "抗故障", "相对随机提升"]
    polys = []
    for (st, nm, col) in ST_INFO:
        m = D["mc"][(0, st)]
        loss = (m["mean"] - D["mc"][(0, "rand")]["mean"]) / m["mean"] * 100
        f0 = next(v for (fp, v) in fs["lookahead"] if fp == 0.0)
        f5 = next(v for (fp, v) in fs["lookahead"] if fp == 0.05)
        anti_fail = (f0 - f5) / f0 * 100  # 故障率升到5%时的相对降幅，越小越抗故障
        vals = [m["mean"] / 200.0, m["p5"] / 200.0, m["p95"] / 200.0,
                max(0, 1 - anti_fail / 30.0), loss / 30.0]
        polys.append((nm, col, vals))
    save("cumcm2018b-3-fig6", radar("图6 四策略稳健性雷达（第1组，各轴按策略最大值归一化）", axes_lab, polys))
    # fig7 热力（三组×四策略 MC 均值）
    heat_vals = [[D["mc"][(gi, st)]["mean"] for (st, _, _) in ST_INFO] for gi in range(3)]
    save("cumcm2018b-3-fig7", heat("图7 各策略单班次产量热力图（行=组 列=策略，MC 均值）", GL,
                                   [nm for (_, nm, _) in ST_INFO], heat_vals))
    # fig8 设计建议汇总
    save("cumcm2018b-3-fig8", line(
        "图8 角色分配曲线与设计建议：s1=4 时产量达平台（推荐配置）",
        [("成品数", "#7c3aed", D["assign"])], xlabel="s1 台数（其余为 s2）", ylabel="成品数 / 班"))


if __name__ == "__main__":
    paper1()
    paper2()
    paper3()
    print("DONE fig_2018b : 24 张 SVG 已生成")
