#!/usr/bin/env python3 # -*- coding: utf-8 -*- """六型号总览图(窗长 / τ / 检测延迟)—— 纯 stdlib 手写 SVG 面板 A:测量窗长 与 IIR 时间常数 τ(横向条形,对数刻度) 面板 B:检测延迟 vs Δf/f(各型号按【本型号档0 判据】计算,对数-对数) 转 PNG:chromium --headless=new --disable-gpu --no-sandbox \ --screenshot=docs/img/family-overview.png --window-size=1520,1000 \ file://$PWD/docs/img/family-overview.svg """ import math W, H = 1520, 1100 FONT = "'Noto Serif CJK SC','Noto Sans CJK SC',sans-serif" MONO = "'DejaVu Sans Mono',monospace" out = [] def esc(s): return (s.replace('&', '&').replace('<', '<').replace('>', '>')) def txt(x, y, s, size=14, fill='#222', anchor='start', weight='normal', family=None): out.append('%s' % (x, y, size, fill, anchor, weight, family or FONT, esc(s))) def line(x1, y1, x2, y2, stroke='#999', w=1.0, dash=None): d = ' stroke-dasharray="%s"' % dash if dash else '' out.append('' % (x1, y1, x2, y2, stroke, w, d)) def rect(x, y, w, h, fill='none', stroke='none', rx=3, sw=1.0, op=1.0, dash=None): d = ' stroke-dasharray="%s"' % dash if dash else '' out.append('' % (x, y, w, h, rx, fill, op, stroke, sw, d)) def poly(pts, stroke='#1565c0', w=1.8, fill='none', dash=None): d = ' stroke-dasharray="%s"' % dash if dash else '' s = ' '.join('%.1f,%.1f' % p for p in pts) out.append('' % (s, fill, stroke, w, d)) def dot(x, y, r=4.0, fill='#c1121f'): out.append('' % (x, y, r, fill)) def note(x, y, w, h, title, lines_, color='#0b6'): rect(x, y, w, h, '#fbfdff', color, rx=6, sw=1.2, op=0.95) txt(x + 12, y + 24, title, 14.5, color, weight='bold') for i, s in enumerate(lines_): txt(x + 12, y + 48 + i * 21, s, 13, '#333') # ---- 型号数据(全部取证过;数值见 docs/cross-model-comparison.md)---- # name, 测量窗(ms), 滤波更新节拍(ms), α, 档0 判据(Δf/f %), 进入确认说明, 颜色 M = [ ('DLD154Pro', 8.738, 8.74, 79, 0.1650, '3 tick 确认(5ms tick)', '#6a1b9a'), ('DLD154V4B', 1.092, 10.00, 79, 0.5140, '3~5 tick 确认(10ms tick)', '#2e7d32'), ('PD136HA', 21.845, 21.85, 64, 0.2472, '无(单次越线)', '#e65100'), ('PD132T', 10.923, 10.92, 79, 0.4578, '无(单次越线)', '#1565c0'), ('PD132L', 27.307, 27.31, 79, 0.4578, '无(单次越线)', '#00838f'), ('M1H', 8.192, 8.19, 79, 0.3296, '10 tick 确认(≈0.6s)', '#5d4037'), ] def tau_line(tick, alfa): return tick * 256.0 / alfa def delay_ms(m, d_pct): """判据越线延迟:d_pct 以 % 传入;返回 (窗数, ms) 或 None(检不到)""" tick, alfa, thr = m[2], m[3], m[4] if d_pct < thr: return None y = 0.0 for n in range(1, 4001): y = d_pct * (1.0 - (1.0 - alfa / 256.0) ** n) if y >= thr: return (n, n * tick) return (4000, 4000 * tick) # ================== 画布 ================== out.append('' % (W, H, W, H)) out.append('' % (W, H)) txt(30, 44, '车检器六型号总览:测量窗长 · IIR 时间常数 · 检测延迟', 25, '#111', weight='bold') txt(30, 72, '窗长 = ⌊BASE/Xn⌋·Xn/f_cap(标称值);τ = 节拍×256/α(产品线口径);检测延迟 = 判据越线时刻(不含输出级 50ms 节拍与进入确认)', 13, '#555') line(30, 86, W - 30, 86, '#ddd', 1.2) # ================== 面板 A:窗长 / τ 条形 ================== txt(40, 118, '面板 A:测量窗长与 IIR τ(对数刻度,单位 ms)', 16, '#111', weight='bold') LA0, LA1 = 250.0, 700.0 ALO, AHI = 1.0, 200.0 lax = lambda v: LA0 + (math.log10(max(v, ALO)) / math.log10(AHI / ALO)) * (LA1 - LA0) # 网格与刻度 for g in (1, 2, 5, 10, 20, 50, 100, 200): x = lax(g) line(x, 140, x, 560, '#e8e8e8', 1.0) txt(x, 578, ('%g' % g), 12.5, '#777', anchor='middle') txt(LA1 + 42, 578, 'ms', 12.5, '#777', anchor='middle') ROW0, ROWH = 152.0, 68.0 for i, m in enumerate(M): nm, win, tick, alfa, thr, conf, col = m y0 = ROW0 + i * ROWH txt(240, y0 + 20, nm, 15, '#111', anchor='end', weight='bold') # 窗长 w1 = max(lax(win) - LA0, 2.0) rect(LA0, y0 + 2, w1, 15, col, rx=2, op=0.95) txt(lax(win) + 6, y0 + 14, '%.2f' % win, 12.5, col) # τ tau = tau_line(tick, alfa) w2 = max(lax(tau) - LA0, 2.0) rect(LA0, y0 + 24, w2, 15, col, rx=2, op=0.38) txt(lax(tau) + 6, y0 + 36, 'τ %.1f' % tau, 12.5, '#555') # 节拍标注(V4B 窗≠节拍) if abs(win - tick) > 0.05: txt(LA0 - 6, y0 + 36, '节拍 %.0f' % tick, 11.5, '#c1121f', anchor='end') line(LA0, 140, LA0, 560, '#bbb', 1.4) txt(250, 596, '深色 = 测量窗长;浅色 = IIR τ;红字 = 滤波更新节拍(与窗长不同者)', 12.5, '#666') # ================== 面板 B:检测延迟 vs Δf/f ================== txt(800, 118, '面板 B:检测延迟 vs 车辆信号 Δf/f(各型号按本型号档0 判据,对数-对数)', 16, '#111', weight='bold') BX0, BX1 = 880.0, 1470.0 BYB, BYT = 545.0, 160.0 RLO, RHI = 0.02, 5.0 DLO, DHI = 4.0, 2000.0 bx = lambda v: BX0 + (math.log10(v / RLO) / math.log10(RHI / RLO)) * (BX1 - BX0) by = lambda d: BYB - (math.log10(max(d, DLO) / DLO) / math.log10(DHI / DLO)) * (BYB - BYT) for g in (0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5): x = bx(g) line(x, BYT, x, BYB, '#e8e8e8', 1.0) txt(x, BYB + 20, ('%g' % g), 12.5, '#777', anchor='middle') txt((BX0 + BX1) / 2, BYB + 42, '车辆引起的相对频偏 Δf/f(%,对数)', 13, '#555', anchor='middle') for g in (4, 10, 30, 100, 300, 1000, 2000): y = by(g) line(BX0, y, BX1, y, '#e8e8e8', 1.0) txt(BX0 - 8, y + 4, ('%g' % g), 12.5, '#777', anchor='end') txt(BX0 - 8, BYT - 14, '延迟 ms', 13, '#555', anchor='end') # 典型车信号带 rect(bx(0.1), BYT, bx(1.0) - bx(0.1), BYB - BYT, '#fff8e1', rx=0, op=0.9) txt((bx(0.1) + bx(1.0)) / 2, BYT + 18, '典型车信号带 0.1%~1%', 12.5, '#b8860b', anchor='middle') # 各型号曲线 for m in M: nm, win, tick, alfa, thr, conf, col = m pts = [] v = thr * 1.002 while v <= RHI: r = delay_ms(m, v) if r is None: v *= 1.12 continue d = r[1] if d > DHI: pts = [] # 越过上限 ⇒ 断开(避免画出界) v *= 1.10 continue pts.append((bx(v), by(d))) v = v * 1.06 + 0.0015 if len(pts) > 1: poly(pts, col, 2.2) # 1 窗下限虚线(标签统一放到图例,避免互相压字) line(bx(thr), by(tick), bx(min(thr * 3.2, RHI)), by(tick), col, 1.0, '3,3') # 阈值竖线 for m in M: x = bx(m[4]) line(x, BYT, x, BYB, m[6], 1.0, '2,3') txt(BX0 + 6, BYB - 8, '竖虚线 = 各型号档0 判据(左端=永不检出);横虚线 = 该型号 1 窗下限', 12, '#666') # 图例(放在面板 B 左上空白区) rect(890, 176, 236, 166, '#ffffff', '#ccc', rx=5, sw=1.0, op=0.94) txt(902, 196, '型号 / 窗长', 13, '#555', weight='bold') for i, m in enumerate(M): yy = 220 + i * 20 rect(902, yy - 9, 14, 11, m[6], rx=2, op=0.95) txt(924, yy, '%s %.2f ms' % (m[0], m[1]), 12, m[6]) # ================== 表格:精确数值 ================== TY = 646.0 line(40, TY - 10, W - 40, TY - 10, '#ddd', 1.2) txt(40, TY + 14, '精确数值(判据越线延迟;不含输出级与进入确认)', 16, '#111', weight='bold') COLS = [(60, '型号'), (190, '测量窗'), (285, '滤波节拍'), (370, 'α'), (412, 'τ(口径)'), (500, '档0 判据'), (620, '延迟 @1.1×判据'), (775, '延迟 @2×判据'), (925, '延迟 @10×判据'), (1060, '进入确认')] for x, s in COLS: txt(x, TY + 46, s, 13.5, '#555', weight='bold') line(40, TY + 56, W - 40, TY + 56, '#ccc', 1.0) for i, m in enumerate(M): nm, win, tick, alfa, thr, conf, col = m y = TY + 84 + i * 40 txt(60, y, nm, 14, col, weight='bold') txt(190, y, '%.2f' % win, 13, '#333') txt(285, y, '%.2f' % tick, 13, '#333') txt(370, y, str(alfa), 13, '#333') txt(412, y, '%.1f' % tau_line(tick, alfa), 13, '#333') txt(500, y, '%.4f %%' % thr, 13, '#333') for dx, mul in ((620, 1.1), (775, 2.0), (925, 10.0)): r = delay_ms(m, thr * mul) if r is None: txt(dx, y, '—', 13, '#c1121f') else: txt(dx, y, '%.1f ms(%d 窗)' % (r[1], r[0]), 13, ('#1565c0' if mul < 1.5 else '#333')) txt(1060, y, conf, 12.5, '#666') line(40, TY + 84 + len(M) * 40 - 22, W - 40, TY + 84 + len(M) * 40 - 22, '#ccc', 1.0) # ================== 脚注 ================== note(40, TY + 84 + len(M) * 40 - 6, 700, 118, '口径与注意', [ 'τ 为产品线口径(节拍×256/α);精确 τ = −节拍/ln(1−α/256),约小 13%', 'V4B 测量窗 1.09ms 与滤波节拍 10ms 不同(Value 每 1.09ms、CAPVD 每 10ms)', '延迟仅含「判据越线」;输出级 50ms 节拍与进入确认未计(见右侧)', '「档0」各型号判据不同(0.165%~0.514%),仅作同档位号横向对比', ], '#c1121f') note(760, TY + 84 + len(M) * 40 - 6, 720, 118, '进入确认(须另加)', [ 'M1H:IN_DELAY=10 tick(约 0.6s);V4B:3~5 tick(30~50ms)', '154Pro:3 次确认;PD132L / PD132T / PD136HA:无(单次越线判定)', '⇒ 整机响应 = 判据越线 + 进入确认 + ≤50ms 输出节拍', ], '#0b6') OPEN = 'docs/img/family-overview.svg' out.append('') open(OPEN, 'w', encoding='utf-8').write('\n'.join(out)) print('已写出 %s(%d 元素)' % (OPEN, len(out))) for m in M: nm = m[0] f = lambda r: ('%.1f ms(%d 窗)' % (r[1], r[0])) if r else '—' print(' %-11s 窗 %6.2f | 节拍 %6.2f | τ %6.1f | 档0 %6.4f%% | @1.1x: %-16s @2x: %-16s @10x: %s' % (nm, m[1], m[2], tau_line(m[2], m[3]), m[4], f(delay_ms(m, m[4] * 1.1)), f(delay_ms(m, m[4] * 2.0)), f(delay_ms(m, m[4] * 10.0))))