# -*- coding: utf-8 -*-
"""
FB_PumpRunLog (v2) 仿真验证脚本
------------------------------------------------------------------
用 Python 1:1 复刻 FB_PumpRunLog v2 的 SCL 逻辑（环形缓冲 600、
默认间隔 1 s、"只在需要时重算"的闸门、窗口钳位、统计遍历），
逐场景验证并给出对照实验。

v2 相对 v1 的改动：深度 100->600、默认间隔 10s->1s、USINT->UINT、
重排与统计从"每扫描"改为"仅采样/复位/窗口变更时"。

运行：
  "C:/Users/你的用户名/.workbuddy/binaries/python/versions/3.13.12/python.exe" test_PumpRunLog_simulation.py
"""

DEPTH = 600          # 缓冲深度（v2 由 100 改为 600）
DEF_INT = 1000       # 默认采样间隔 ms（v2 由 10000 改为 1000）


class PumpRunLog:
    """1:1 复刻 FB_PumpRunLog v2。时间单位统一用毫秒整数，与 PLC 的 TIME 一致。"""

    def __init__(self):
        self.aBuf = [False] * (DEPTH + 1)
        self.iHead = 1
        self.iFill = 0
        self.tAcc = 0
        self.uLastWin = 0
        self.iLastAgeSt = -1
        self.byVer = 0x02
        # 输出（静态保持：未重算的扫描沿用上次值）
        self.out = {
            'bAnyRun': False, 'bNeverRun': False, 'bValid': False,
            'uRunCnt': 0, 'uWinSize': 0, 'uFilled': 0, 'rRunRate': 0.0,
            'tWindowSpan': 0, 'tSinceLastRun': 0, 'tToNextSample': 0,
            'aHist': [False] * (DEPTH + 1),
            'bSample': False, 'bCfgFault': False, 'bWarn': False,
        }
        self.recalc_count = 0        # 统计重算次数（用于验证闸门真的生效）

    def scan(self, bPumpRun, bEnable=True, bReset=False,
             tInterval=DEF_INT, tCycle=100, uWindow=DEPTH, gate=True):
        """gate=False 时模拟 v1 的"每扫描都重算"，用于等价性对照。"""
        o = self.out
        o['bSample'] = False

        # ---- 1. 配置检查 ----
        o['bCfgFault'] = (tInterval <= 0) or (tCycle <= 0)
        o['bWarn'] = (not o['bCfgFault']) and (tInterval < tCycle)

        # ---- 1.1 计算用副本（防 TIME 乘法溢出：T#50min = 3_000_000 ms）----
        tIntEff = min(tInterval, 3_000_000)

        # ---- 2. 复位 ----
        if bReset:
            for i in range(1, DEPTH + 1):
                self.aBuf[i] = False
            self.iHead = 1
            self.iFill = 0
            self.tAcc = 0

        # ---- 3. 采样节拍 ----
        if o['bCfgFault']:
            self.tAcc = 0
        elif bEnable and not bReset:
            self.tAcc += tCycle
            if self.tAcc >= tInterval:
                if tInterval <= tCycle:
                    self.tAcc = 0
                else:
                    self.tAcc -= tInterval
                self.aBuf[self.iHead] = bPumpRun
                self.iHead += 1
                if self.iHead > DEPTH:
                    self.iHead = 1
                if self.iFill < DEPTH:
                    self.iFill += 1
                o['bSample'] = True

        # ---- 4+5. 闸门：只在采样 / 复位 / 窗口变更时重算 ----
        need = o['bSample'] or bReset or (uWindow != self.uLastWin) or (not gate)
        if need:
            self.recalc_count += 1
            self.uLastWin = uWindow

            iWreq = min(max(uWindow, 1), DEPTH)
            iW = min(iWreq, self.iFill)
            o['uWinSize'] = iW
            o['uFilled'] = self.iFill
            o['bValid'] = self.iFill >= iWreq

            aHist = [False] * (DEPTH + 1)
            iNewest = self.iHead - 1
            if iNewest < 1:
                iNewest = DEPTH
            iCnt, iAge = 0, -1
            o['bAnyRun'] = False
            for iIdx in range(1, iW + 1):
                iPos = iNewest - (iIdx - 1)
                while iPos < 1:
                    iPos += DEPTH
                bVal = self.aBuf[iPos]
                aHist[DEPTH + 1 - iIdx] = bVal
                if bVal:
                    iCnt += 1
                    o['bAnyRun'] = True
                    if iAge < 0:
                        iAge = iIdx - 1
            o['aHist'] = aHist
            self.iLastAgeSt = iAge
            o['uRunCnt'] = iCnt
            o['bNeverRun'] = o['bValid'] and (not o['bAnyRun'])
            o['rRunRate'] = (iCnt / iW * 100.0) if iW > 0 else 0.0
            o['tWindowSpan'] = tIntEff * iW

        # ---- 6. 时间类输出（每个扫描都更新）----
        if self.iLastAgeSt >= 0:
            o['tSinceLastRun'] = tIntEff * self.iLastAgeSt + self.tAcc
        else:
            o['tSinceLastRun'] = 0
        o['tToNextSample'] = 0 if self.tAcc >= tInterval else tInterval - self.tAcc
        return o


def ms(t):
    if t < 1000:
        return f'{t}ms'
    if t < 60000:
        return f'{t/1000:.2f}s'
    return f'{t/60000:.2f}min'


# ==================================================================
def scenario_1():
    print('=' * 84)
    print('场景 1  采样间隔精度对照：tAcc 减法(本块做法) vs 清零(常见写法)')
    print('=' * 84)
    print('参数：tInterval = 1.05 s（刻意取一个除不尽的间隔）, tCycle = 100 ms')
    print('      跑 2 小时（7200 s），看绝对时刻的累计误差\n')

    tInterval, tCycle, total = 1050, 100, 7_200_000
    for mode, label in (('sub', '减法（本块做法）'), ('zero', '清零（常见写法）')):
        fb = PumpRunLog()
        stamps, t = [], 0
        while t < total:
            o = fb.scan(True, tInterval=tInterval, tCycle=tCycle, uWindow=DEPTH)
            t += tCycle
            if o['bSample']:
                stamps.append(t)
        n = len(stamps)
        avg = stamps[-1] / n
        err = stamps[-1] / 1000.0 - n * tInterval / 1000.0
        print(f'  {label}')
        print(f'    2 小时内采样次数 : {n}')
        print(f'    平均间隔         : {avg:.2f} ms  （设置值 {tInterval} ms，偏差 {avg-tInterval:+.2f} ms）')
        print(f'    累计误差         : {err:+.1f} s   （{err/(stamps[-1]/1000.0)*100:+.4f}%）')
    print('\n  理论：减法零系统误差；清零把 1050 ms 向上取整到 1100 ms（+4.76%），')
    print('        预期 2 小时累计误差 = 7200 x 0.0476 ≈ 343 s。')
    print('  -> 这个误差是"每次采样丢掉的零头"造成的，与缓冲深度无关，')
    print('     所以 v2 把深度翻到 600 并没有放大它。\n')


# ==================================================================
def scenario_2():
    print('=' * 84)
    print('场景 2  窗口滑动：泵"运行 5 min / 停 5 min"循环，看 bAnyRun 何时翻转')
    print('=' * 84)
    print(f'参数：tInterval = {DEF_INT} ms (1 s), tCycle = 100 ms, 仿真 2 小时')
    print('泵模式：每 600 s 一个周期，前 300 s 运行、后 300 s 停止\n')

    def pump(t):
        return (t % 600) < 300

    for uWindow in (600, 300, 240):
        fb = PumpRunLog()
        flips, prev, t = [], None, 0
        while t < 7_200_000:
            v = fb.scan(pump(t / 1000.0), tInterval=DEF_INT, tCycle=100, uWindow=uWindow)
            t += 100
            if v['bSample']:
                if prev is not None and v['bAnyRun'] != prev:
                    flips.append((t / 1000.0, v['bAnyRun']))
                prev = v['bAnyRun']
        print(f'  uWindow = {uWindow:3d}  ->  窗口 {uWindow*DEF_INT/1000:5.0f} s '
              f'({uWindow*DEF_INT/60000:4.1f} min)   bAnyRun 翻转 {len(flips):3d} 次   '
              f'窗口填满(bValid) 约 {uWindow*DEF_INT/1000:.0f}s')
    print('\n  结论：窗口时长必须小于"你最关心的停机时长"，bAnyRun 才会真的翻转。')
    print('        泵停 5 min、窗口 5 min 时恒为 TRUE（窗口里总还留着运行段）；')
    print('        窗口收到 4 min 才开始在停机段翻 FALSE。')
    print('        调灵敏度请改 uWindow，不要改 tInterval（后者会改变整体时间尺度）。\n')


# ==================================================================
def scenario_3():
    print('=' * 84)
    print('场景 3  环形回绕 + aHist 顺序正确性（600 格，最容易写错的地方）')
    print('=' * 84)

    fb = PumpRunLog()
    expect, k, t = [], 0, 0
    while k < 2000:                       # 跑 2000 次采样，回绕 3.3 圈
        v = (k // 7) % 2 == 0
        o = fb.scan(v, tInterval=1000, tCycle=100, uWindow=DEPTH)
        if o['bSample']:
            expect.append(v)
            k += 1
        t += 100
    got = o['aHist'][1:DEPTH + 1]
    want = expect[-DEPTH:]
    print(f'  跑了 {k} 次采样（回绕 3.3 圈），iHead 最终 = {fb.iHead}，iFill = {fb.iFill}')
    print(f'  aHist 与"最近 600 个采样(旧->新)"逐位比对：{"完全一致 OK" if got == want else "不一致 FAIL"}')

    fb2 = PumpRunLog()
    kk = 0
    while kk < DEPTH:
        o2 = fb2.scan(kk % 2 == 1, tInterval=1000, tCycle=100, uWindow=DEPTH)
        if o2['bSample']:
            kk += 1
    print(f'  第 600 次采样后 iHead = {fb2.iHead}（应为 1，刚好转完一圈）')
    while True:
        o2 = fb2.scan(True, tInterval=1000, tCycle=100, uWindow=DEPTH)
        if o2['bSample']:
            break
    print(f'  第 601 次采样后 iHead = {fb2.iHead}（应为 2），'
          f'aHist[600] = {o2["aHist"][DEPTH]}（应为 True，即最新值）')
    print(f'  uFilled = {o2["uFilled"]}（应为 600，封顶不再增长）')
    print()


# ==================================================================
def scenario_4():
    print('=' * 84)
    print('场景 4  边界与异常（含 v2 新增的 UINT 类型边界）')
    print('=' * 84)

    fb = PumpRunLog()
    o = fb.scan(True, tInterval=0)
    print(f'  4.1 tInterval = 0        -> bCfgFault={o["bCfgFault"]}, bSample={o["bSample"]}, '
          f'uFilled={o["uFilled"]}')
    o = fb.scan(True, tInterval=DEF_INT, tCycle=0)
    print(f'      tCycle = 0           -> bCfgFault={o["bCfgFault"]}')

    fb = PumpRunLog()
    o = fb.scan(True, tInterval=50, tCycle=100)
    print(f'  4.2 tInterval(50ms) < tCycle(100ms) -> bWarn={o["bWarn"]}, bSample={o["bSample"]}')

    fb = PumpRunLog()
    t = 0
    while t < 60_000:
        fb.scan(True, tInterval=DEF_INT, tCycle=100); t += 100
    fill_before, acc_before = fb.iFill, fb.tAcc
    t = 0
    while t < 600_000:                    # 冻结 10 分钟
        fb.scan(True, bEnable=False, tInterval=DEF_INT, tCycle=100); t += 100
    print(f'  4.3 bEnable=FALSE 冻结 10 min -> 已填样本 {fill_before} 条保持不变({fb.iFill})，'
          f'tAcc 冻结在 {acc_before}ms({fb.tAcc}ms)')

    o = fb.scan(True, bReset=True, tInterval=DEF_INT, tCycle=100)
    print(f'  4.4 复位 -> uFilled={o["uFilled"]}, iHead={fb.iHead}, tAcc={fb.tAcc}ms, '
          f'bAnyRun={o["bAnyRun"]}, 复位周期内 bSample={o["bSample"]}（不再写新数据）')

    fb = PumpRunLog()
    o = fb.scan(True)
    print(f'  4.5 首次扫描（空缓冲）-> uWinSize={o["uWinSize"]}, bAnyRun={o["bAnyRun"]}, '
          f'bValid={o["bValid"]}, rRunRate={o["rRunRate"]}, tWindowSpan={o["tWindowSpan"]}（不崩不除零）')

    fb = PumpRunLog()
    kk = 0
    while kk < DEPTH:
        o = fb.scan(False, tInterval=1000, tCycle=100, uWindow=9999)
        if o['bSample']:
            kk += 1
    print(f'  4.6 uWindow = 9999（远超 600）-> 当作 600 用，uWinSize={o["uWinSize"]}，不数组越界')
    fb = PumpRunLog()
    o = fb.scan(False, tInterval=1000, tCycle=100, uWindow=0)
    print(f'      uWindow = 0          -> 钳到 1，uWinSize={o["uWinSize"]}，不除零')

    TIME_MAX, CLAMP600 = 2 ** 31 - 1, 3_000_000

    def as_time(v):
        v &= 0xFFFFFFFF
        return v - 2 ** 32 if v >= 2 ** 31 else v

    print('  4.7 TIME 乘法溢出（600 格，阈值 T#50min = 3,000,000 ms）：')
    for iv, lbl in ((1000, 'T#1s'), (1_800_000, 'T#30min'), (3_600_000, 'T#1h')):
        raw, cl = as_time(iv * DEPTH), as_time(min(iv, CLAMP600) * DEPTH)
        flag = '  <- 未钳位翻负!' if raw < 0 else ''
        print(f'      tInterval={lbl:8s} 未钳位={raw:>14,d} ms{flag}   钳位后={cl:>13,d} ms'
              f' = {cl/86_400_000:5.1f} 天')
    print(f'      （TIME 上限 {TIME_MAX:,d} ms ≈ 24.8 天。v1 深度 100 时阈值是 T#5h，')
    print('        v2 深度 600 后必须降到 T#50min，否则 tInterval=T#1h 就会溢出）')
    print()


# ==================================================================
def scenario_5():
    print('=' * 84)
    print('场景 5  典型工况（tInterval=1s, uWindow=600 -> 窗口 10 min）')
    print('=' * 84)
    cases = [
        ('连续运行',         lambda t: True,           600),
        ('连续停机',         lambda t: False,          600),
        ('每 2min 运行 30s', lambda t: (t % 120) < 30, 600),
        ('每 2min 运行 2s',  lambda t: (t % 120) < 2,  600),
    ]
    for name, fn, uw in cases:
        fb = PumpRunLog()
        t = 0
        while t < 2 * 3_600_000:
            o = fb.scan(fn(t / 1000.0), tInterval=DEF_INT, tCycle=100, uWindow=uw)
            t += 100
        print(f'  {name:<16s} bAnyRun={str(o["bAnyRun"]):<5s} bValid={str(o["bValid"]):<5s} '
              f'bNeverRun={str(o["bNeverRun"]):<5s} 运行点={o["uRunCnt"]:3d}/600 '
              f'占比={o["rRunRate"]:5.1f}% 窗口={ms(o["tWindowSpan"])}')
    print()
    print('  注意第 4 行：每 2 min 只运行 2 s，真实时间占比 1.7%。')
    print('  v2 默认间隔 1 s，2 s 的运行仍有约一半概率整段落在两次采样之间 ——')
    print('  这类"短时动作"用采样式记录本质上是抓不稳的，')
    print('  要稳稳抓住得用边沿记录块 FB_PumpEdgeLog（见它的仿真脚本场景 1）。')
    print()


# ==================================================================
def scenario_6():
    print('=' * 84)
    print('场景 6  v2 优化等价性：闸门(只在需要时重算) vs 每扫描都重算')
    print('=' * 84)
    print('参数：tInterval = 1 s, tCycle = 100 ms, uWindow = 600, 仿真 30 分钟')
    print('泵模式：每 120 s 运行 30 s\n')

    def pump(t):
        return (t % 120) < 30

    res = {}
    for gate in (True, False):
        fb = PumpRunLog()
        t = 0
        while t < 1_800_000:
            o = fb.scan(pump(t / 1000.0), tInterval=DEF_INT, tCycle=100,
                        uWindow=DEPTH, gate=gate)
            t += 100
        res[gate] = (dict(o), fb.recalc_count)

    a, ca = res[True]
    b, cb = res[False]
    keys = ('bAnyRun', 'uRunCnt', 'uWinSize', 'uFilled', 'bValid', 'bNeverRun')
    same = all(a[k] == b[k] for k in keys) and abs(a['rRunRate'] - b['rRunRate']) < 1e-9
    print(f'  闸门开（v2 做法）：统计重算 {ca:6d} 次')
    print(f'  闸门关（v1 做法）：统计重算 {cb:6d} 次（= 扫描次数）')
    print(f'  省下的重算次数   ：{cb - ca} 次，占比 {(1-ca/cb)*100:.2f}%')
    print(f'  两者最终结论逐项一致：{"是 OK" if same else "否 FAIL"}')
    for k in keys:
        print(f'      {k:10s} 开={str(a[k]):<8s} 关={str(b[k]):<8s} '
              f'{"一致" if a[k] == b[k] else "不一致!"}')
    print(f'      rRunRate   {a["rRunRate"]:.4f}   {b["rRunRate"]:.4f}')
    print()
    print('  结论：两次采样之间缓冲一个比特都没变，结论不可能变。闸门把 600 格的')
    print('        清空+遍历从"每扫描"降到"每采样一次"，结论逐项完全一致，')
    print('        而扫描周期越短收益越大（10 ms 周期下相当于省掉一成多的扫描时间）。')
    print()


# ==================================================================
def scenario_7():
    print('=' * 84)
    print('场景 7  导出 SVG 状态条带图')
    print('=' * 84)

    def pump(t):
        return (t % 600) < 300

    fb = PumpRunLog()
    uw, rec, t = 240, [], 0
    while t < 1_800_000:
        o = fb.scan(pump(t / 1000.0), tInterval=DEF_INT, tCycle=100, uWindow=uw)
        t += 100
        if o['bSample']:
            rec.append((t / 1000.0, pump(t / 1000.0), o['bAnyRun'], o['bValid']))

    W, H = 1080, 200
    x0, y0, bw, bh = 120, 46, 900, 30
    n = len(rec)
    step = bw / n
    c_run, c_stop, c_ok, c_bad, c_gap = '#16a34a', '#cbd5e1', '#2563eb', '#f59e0b', '#e2e8f0'
    p = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}">',
         f'<rect width="{W}" height="{H}" fill="#ffffff"/>',
         '<text x="120" y="24" font-family="-apple-system,Segoe UI,Roboto,sans-serif" '
         'font-size="15" font-weight="600" fill="#1f2937">'
         f'FB_PumpRunLog v2 窗口滑动验证（泵：运行 5min / 停 5min，uWindow={uw} → 窗口 240s）</text>',
         '<text x="114" y="66" text-anchor="end" font-family="sans-serif" font-size="12" '
         'fill="#4b5563">泵状态</text>',
         f'<rect x="{x0}" y="{y0}" width="{bw}" height="{bh}" fill="#f1f5f9"/>']
    for i, (tt, run, _, _) in enumerate(rec):
        col = c_run if run else c_stop
        p.append(f'<rect x="{x0 + i*step:.2f}" y="{y0}" width="{step+0.4:.2f}" height="{bh}" fill="{col}"/>')
    y1 = y0 + bh + 26
    p.append(f'<text x="114" y="{y1+21}" text-anchor="end" font-family="sans-serif" '
             'font-size="12" fill="#4b5563">bAnyRun</text>')
    p.append(f'<rect x="{x0}" y="{y1}" width="{bw}" height="{bh}" fill="#f1f5f9"/>')
    for i, (tt, run, ar, valid) in enumerate(rec):
        col = c_gap if not valid else (c_ok if ar else c_bad)
        p.append(f'<rect x="{x0 + i*step:.2f}" y="{y1}" width="{step+0.4:.2f}" height="{bh}" fill="{col}"/>')
    prev = None
    for i, (tt, run, ar, valid) in enumerate(rec):
        if prev is not None and ar != prev:
            xx = x0 + i * step
            p.append(f'<line x1="{xx:.1f}" y1="{y0-6}" x2="{xx:.1f}" y2="{y1+bh+6}" '
                     'stroke="#ef4444" stroke-width="1.2" stroke-dasharray="4,3"/>')
            txt = '-> TRUE' if ar else '-> FALSE'
            p.append(f'<text x="{xx+4:.1f}" y="{y1+bh+20}" font-family="sans-serif" font-size="11" '
                     f'fill="#ef4444">{txt} @ {tt:.0f}s</text>')
        prev = ar
    ly, lx = H - 16, 120
    for col, lb in [(c_run, '运行'), (c_stop, '停止'), (c_gap, '窗口未填满'),
                    (c_ok, 'bAnyRun=TRUE'), (c_bad, 'bAnyRun=FALSE')]:
        p.append(f'<rect x="{lx}" y="{ly-9}" width="12" height="12" rx="2" fill="{col}"/>')
        p.append(f'<text x="{lx+17}" y="{ly+1}" font-family="sans-serif" font-size="11" '
                 f'fill="#4b5563">{lb}</text>')
        lx += 30 + len(lb) * 12
    p.append(f'<text x="{W-16}" y="{ly+1}" text-anchor="end" font-family="sans-serif" font-size="11" '
             f'fill="#94a3b8">共 {n} 次采样 / 30 min</text></svg>')

    out = r'D:\你的工作目录\04_文档\FB_PumpRunLog_窗口滑动验证.svg'
    with open(out, 'w', encoding='utf-8') as f:
        f.write('\n'.join(p))
    print(f'  已导出: {out}')
    print(f'  采样点数 {n}，bValid 首次为 TRUE 的时刻 = {rec[uw-1][0]:.0f}s\n')


if __name__ == '__main__':
    scenario_1()
    scenario_2()
    scenario_3()
    scenario_4()
    scenario_5()
    scenario_6()
    scenario_7()
    print('=' * 84)
    print('FB_PumpRunLog v2 全部场景验证完毕')
    print('=' * 84)
