Files
vd-analysis/tools/plot_family_overview.py
wangfq db5e108620 fig(family): 六型号总览图 + PD132T↔PD136HA 档位对齐图;待实测清单 ⑧
- docs/img/family-overview.{svg,png}(tools/plot_family_overview.py)
  面板A 窗长/τ 条形;面板B 检测延迟 vs Δf/f(六型号按各自档0 判据);
  归一化延迟表(@1.1× / @2× / @10× 判据)+ 进入确认与口径脚注
  关键:@10×(强信号) 全部=1 窗 ⇒ 8.2/8.7/10.0/10.9/21.9/27.3ms;@1.1×(近阈值) 7~9 窗 ⇒ M1H 57ms ~ PD136HA 197ms
- docs/img/sens-alignment-pd132t-pd136ha.{svg,png}(tools/plot_sens_alignment.py)
  四档 进入/离开 双段条形 + 同档偏差(−46%~+69%) + 最近档映射 + 换型 4 步 + 两端档无对应警示
- 待实测清单新增 ⑧ 弱信号复测(0.10% 车:PD132T 档1 ≈76ms 应检出 / PD136HA 档1 应检不到)
- 文档接入:cross-model-comparison.md 新增「六型号总览图」节;comparison §5.4 嵌入对齐图;measurement doc §10 加指引
- README tools 段补 3 个绘图脚本;表格校验 0 异常(16 文件)
2026-09-11 11:27:58 +08:00

224 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""六型号总览图(窗长 / τ / 检测延迟)—— 纯 stdlib 手写 SVG
面板 A:测量窗长 与 IIR 时间常数 τ(横向条形,对数刻度)
面板 B:检测延迟 vs Δf/f(各型号按【本型号档0 判据】计算,对数-对数)
转 PNGchromium --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('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;'))
def txt(x, y, s, size=14, fill='#222', anchor='start', weight='normal', family=None):
out.append('<text x="%.1f" y="%.1f" font-size="%s" fill="%s" text-anchor="%s" font-weight="%s" font-family="%s">%s</text>'
% (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('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%s"%s/>'
% (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('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%s" fill="%s" fill-opacity="%s" stroke="%s" stroke-width="%s"%s/>'
% (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('<polyline points="%s" fill="%s" stroke="%s" stroke-width="%s" stroke-linejoin="round"%s/>'
% (s, fill, stroke, w, d))
def dot(x, y, r=4.0, fill='#c1121f'):
out.append('<circle cx="%.1f" cy="%.1f" r="%.1f" fill="%s"/>' % (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('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d">' % (W, H, W, H))
out.append('<rect width="%d" height="%d" fill="#ffffff"/>' % (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, '进入确认(须另加)', [
'M1HIN_DELAY=10 tick(约 0.6s);V4B3~5 tick30~50ms',
'154Pro3 次确认;PD132L / PD132T / PD136HA:无(单次越线判定)',
'⇒ 整机响应 = 判据越线 + 进入确认 + ≤50ms 输出节拍',
], '#0b6')
OPEN = 'docs/img/family-overview.svg'
out.append('</svg>')
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))))