Files
vd-analysis/tools/plot_response_compare.py
wangfq 9c88b1d7b0 fig(compare): PD132T↔PD136HA 响应对比图(时域 + 延迟扫描)
- docs/img/response-compare-pd132t-pd136ha.{svg,png},生成脚本 tools/plot_response_compare.py
- 面板A:CAPVD 阶梯响应(+0.5% 与 +0.15%);面板B:检测延迟 vs Δf/f(对数扫描,含 τ 主导陡升段与阈值以下灰区)
- 数值:+0.5% ⇒ 10.9 vs 21.8 ms(窗长主导,2.0×);+0.15% ⇒ 32.6 vs 152.3 ms(τ 主导,4.7×);+0.10% ⇒ PD132T 76.2 ms / PD136HA 档1 检不到
- 落文档:pd132t-vs-pd136ha-comparison.md §5.5 新增;measurement-window-quantization.md §10 新增;README tools 补脚本
- 表格校验 0 异常(16 文件)
2026-09-11 11:15:09 +08:00

210 lines
9.8 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 -*-
"""PD132T vs PD136HA 响应对比图(纯 stdlib 手写 SVG
面板 A:同一判据下的 CAPVD 响应(时域;Δf/f = +0.5% 大信号 与 +0.15% 近阈值两组)
面板 B:检测延迟 vs Δf/f(对数扫描)—— 看「窗主导」与「τ 主导」的分界
转 PNGchromium --headless=new --disable-gpu --no-sandbox \
--screenshot=docs/img/response-compare.svg.png --window-size=1520,980 \
file://$PWD/docs/img/response-compare-pd132t-pd136ha.svg
"""
import math
FONT = "'Noto Serif CJK SC','Source Han Serif SC','WenQuanYi Zen Hei',sans-serif"
W, H = 1520.0, 980.0
o = []
def add(s): o.append(s)
def esc(s): return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
def txt(x, y, s, fs=14, fill='#222', anchor='start', w='400'):
add(f'<text x="{x:.1f}" y="{y:.1f}" font-family="{FONT}" font-size="{fs}" '
f'fill="{fill}" text-anchor="{anchor}" font-weight="{w}">{esc(s)}</text>')
def line(x1, y1, x2, y2, col='#888', w=1, dash=None):
d = f' stroke-dasharray="{dash}"' if dash else ''
add(f'<line x1="{x1:.1f}" y1="{y1:.1f}" x2="{x2:.1f}" y2="{y2:.1f}" stroke="{col}" '
f'stroke-width="{w}"{d}/>')
def rect(x, y, w_, h_, fill='none', stroke='#999', sw=1, rx=4, op=1.0):
add(f'<rect x="{x:.1f}" y="{y:.1f}" width="{w_:.1f}" height="{h_:.1f}" rx="{rx}" '
f'fill="{fill}" fill-opacity="{op}" stroke="{stroke}" stroke-width="{sw}"/>')
def poly(pts, col, w=2.2, dash=None):
d = f' stroke-dasharray="{dash}"' if dash else ''
p = ' '.join(f'{x:.1f},{y:.1f}' for x, y in pts)
add(f'<polyline points="{p}" fill="none" stroke="{col}" stroke-width="{w}"{d}/>')
def dot(x, y, r=4.2, col='#c1121f'):
add(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{r}" fill="{col}"/>')
def note(x, y, w_, h_, title, lines_, col='#0b6'):
rect(x, y, w_, h_, fill='#f7fbff', stroke=col)
txt(x + 12, y + 26, title, 15, '#0b4f7a', w='700')
for i, s in enumerate(lines_):
txt(x + 12, y + 52 + i * 21, s, 14, '#333')
# ============ 模型参数(与 measurement-window-quantization.md 同源公式) ============
def model(name, f_cap, edge, alfa, sens_in, color, f_coil=100e3, base=32768, div=32):
xn0 = round(div * f_cap / (edge * f_coil))
lp = base // xn0
tw = lp * xn0 / f_cap
return dict(name=name, color=color, alfa=alfa, thr=sens_in / 65536.0,
tw=tw, xn0=xn0, lp=lp, tau=-tw / math.log(1 - alfa / 256.0),
tau_pl=tw * 256.0 / alfa)
PT = model('PD132T', 3.0e6, 2, 79, 60, '#1565c0')
PH = model('PD136HA', 1.5e6, 1, 64, 81, '#e65100')
def resp(m, df, n):
"""第 n 次(0-basedCAPVD 更新后的 Δf/f(相对值)"""
return df * (1.0 - (1.0 - m['alfa'] / 256.0) ** (n + 1))
def delay_ms(m, df, cap=20000):
"""返回 (窗数-截止更新序号 idx, 时间 ms)idx=None 表示 cap 内不可检"""
n = 0
while n < cap:
if resp(m, df, n) >= m['thr']:
return n, (n + 1) * m['tw'] * 1e3
n += 1
return None, None
# ================== 面板 A:时域响应 ==================
AX0, AX1, AY0, AY1 = 110.0, 700.0, 640.0, 150.0
TMAX = 300.0 # ms
VMAX = 0.62 # %
ax = lambda t: AX0 + (t / TMAX) * (AX1 - AX0) # t in ms
ay = lambda v: AY0 - (v / VMAX) * (AY0 - AY1) # v in %
txt(30, 40, 'PD132T ↔ PD136HA 响应对比:同一判据口径,不同测量节拍与滤波', 21, '#111', w='700')
txt(30, 66, 'PD132T: 窗 10.880 ms / α=79 / τ=29.5 ms(产品线口径 35.3 ms '
'PD136HA: 窗 21.760 ms / α=64 / τ=76.0 ms(产品线口径 87.0 ms 站点 100 kHz', 13.5, '#555')
txt(AX0 - 10, AY1 - 18, '面板 A:CAPVD 响应(时域,一次完全占据)', 15.5, '#0b4f7a', 'start', '700')
rect(AX0, AY1, AX1 - AX0, AY0 - AY1, fill='#fcfcfc', stroke='#ccc')
base_l = AY0 + 38
line(AX0, AY0, AX1, AY0, '#888', 1.5)
line(AX0, AY1, AX0, AY0, '#888', 1.5)
for i in range(7):
v = i * 0.1
line(AX0, ay(v), AX1, ay(v), '#eee', 1)
txt(AX0 - 8, ay(v) + 5, '%.1f%%' % v, 12.5, '#666', 'end')
txt(AX0 + 10, AY1 + 22, 'Δf/f', 13, '#444', w='700')
for t in range(0, 301, 50):
line(ax(t), AY0, ax(t), AY0 + 6, '#888', 1)
txt(ax(t), base_l, str(t), 12.5, '#666', 'middle')
txt((AX0 + AX1) / 2, base_l + 24, '时间(ms', 13.5, '#444', 'middle', '700')
# 阈值线(各型号档1 进入)
line(AX0, ay(PT['thr'] * 100), AX1, ay(PT['thr'] * 100), '#1565c0', 1.4, '6,4')
line(AX0, ay(PH['thr'] * 100), AX1, ay(PH['thr'] * 100), '#e65100', 1.4, '6,4')
txt(ax(205), ay(PT['thr'] * 100) + 20, 'PD132T 档1 0.0916%', 12.5, '#1565c0')
txt(ax(205), ay(PH['thr'] * 100) + 20, 'PD136HA 档1 0.1236%', 12.5, '#e65100')
# 两条响应(阶梯)
def stair_ms(m, df, tmax=TMAX):
pts = [(ax(0.0), ay(0.0))]
t = 0.0
n = 0
while t <= tmax:
pts.append((ax(t), ay(resp(m, df * 100, n) if False else resp(m, df, n) * 100)))
t += m['tw'] * 1e3
n += 1
return pts
for m in (PT, PH):
for df, dash, lw in ((0.005, None, 2.6), (0.0015, '7,5', 1.8)):
pts = [(ax(0.0), ay(0.0))]
t = 0.0
n = 0
while t <= TMAX:
pts.append((ax(t), ay(resp(m, df, n) * 100)))
t += m['tw'] * 1e3
n += 1
poly(pts, m['color'], lw, dash)
# 越线标记
for m, df in ((PT, 0.005), (PH, 0.005), (PT, 0.0015), (PH, 0.0015)):
i, ms = delay_ms(m, df)
if i is None:
continue
dot(ax(ms), ay(resp(m, df, i) * 100), 4.6 if df == 0.005 else 3.4, m['color'])
iT5, msT5 = delay_ms(PT, 0.005); iH5, msH5 = delay_ms(PH, 0.005)
iT15, msT15 = delay_ms(PT, 0.0015); iH15, msH15 = delay_ms(PH, 0.0015)
note(110, 716, 580, 158, '越线时刻与读图要点(同一判据口径)', [
'大信号 +0.5%%PD132T 第 %d 窗 ⇒ %.1f ms PD136HA 第 %d 窗 ⇒ %.1f ms' % (iT5 + 1, msT5, iH5 + 1, msH5),
'近阈值 +0.15%%PD132T 第 %d 窗 ⇒ %.1f ms PD136HA 第 %d 窗 ⇒ %.1f ms' % (iT15 + 1, msT15, iH15 + 1, msH15),
'⇒ 大信号由「窗长」决定(2×);近阈值由「τ」决定(≈4.7×)',
'面板 B:右侧平坦段 = 1 窗下限;左侧陡升 = IIR(τ) 主导;阈值以下永不检到',
], '#0b6')
txt(110, 890, '蓝 = PD132T,橙 = PD136HA;实线 = 大信号(+0.5%),虚线 = 近阈值(+0.15%', 13, '#555')
# ================== 面板 B:检测延迟 vs Δf/f(对数扫描) ==================
BX0, BX1, BYB, BYT = 830.0, 1430.0, 830.0, 170.0
DLO, DHI = 4.0, 3000.0 # ms
RLO, RHI = 0.02, 5.0 # %
bx = lambda v: BX0 + (math.log10(v / RLO) / math.log10(RHI / RLO)) * (BX1 - BX0)
by = lambda d: BYB - (math.log10(d / DLO) / math.log10(DHI / DLO)) * (BYB - BYT)
txt(BX0 - 10, BYT - 18, '面板 B:检测延迟随信号强度(对数扫描)', 15.5, '#0b4f7a', 'start', '700')
rect(BX0, BYT, BX1 - BX0, BYB - BYT, fill='#fcfcfc', stroke='#ccc')
line(BX0, BYB, BX1, BYB, '#888', 1.5)
line(BX0, BYT, BX0, BYB, '#888', 1.5)
for d in (4, 10, 30, 100, 300, 1000, 3000):
line(BX0, by(d), BX1, by(d), '#eee', 1)
txt(BX0 - 8, by(d) + 5, ('%g' % d) + ' ms', 12.5, '#666', 'end')
for v in (0.02, 0.05, 0.1, 0.2, 0.5, 1, 2, 5):
line(bx(v), BYB, bx(v), BYB - 6, '#888', 1)
txt(bx(v), BYB + 22, ('%g' % v) + '%', 12.5, '#666', 'middle')
txt((BX0 + BX1) / 2, BYB + 48, '车辆引起的 Δf/f%,对数轴)', 13.5, '#444', 'middle', '700')
# 典型车型区间
rect(bx(0.1), BYT, bx(1.0) - bx(0.1), BYB - BYT, fill='#f2c200', stroke='none', op=0.13)
txt(bx(0.1) + 6, BYT + 20, '典型车型 0.1%~1%', 12.5, '#8a6d00')
# 曲线
for m in (PT, PH):
pts = []
v = RLO * 1.05
while v <= RHI:
i, ms = delay_ms(m, v / 100.0, cap=4000)
v *= 1.02
if i is None:
continue
if ms > DHI:
break
pts.append((bx(v), by(ms)))
poly(pts, m['color'], 2.6)
# 阈值竖线
thrp = m['thr'] * 100
line(bx(thrp), BYB, bx(thrp), BYT, m['color'], 1.4, '6,4')
anc = 'end' if m is PT else 'start'
off = -6 if m is PT else 6
txt(bx(thrp) + off, BYT - 6, '%s 档1 %.4f%%' % (m['name'], thrp), 12.5, m['color'], anc)
# 1 窗平台标注
line(BX0, by(m['tw'] * 1e3), bx(thrp), by(m['tw'] * 1e3), m['color'], 1.2, '3,4')
txt(bx(thrp) + 8, by(m['tw'] * 1e3) - 7, '≈1 窗 %.2f ms' % (m['tw'] * 1e3), 12.5, m['color'])
# 不可检区域(阈值左侧)
for m in (PT, PH):
rect(BX0, BYT, bx(m['thr'] * 100) - BX0, BYB - BYT, fill='#bbb', stroke='none', op=0.18)
txt(BX0 + 8, BYB - 12, '← 阈值以下:永远检不到(灰区)', 12.5, '#555')
dT_big = iT5 and msT5; dH_big = msH5
# ================== 页脚 ==================
note(30, 912, 700, 62, '公式(与 measurement-window-quantization.md §1~§4 同源)', [
'CAPVD_n = Δf/f·[1 (1−α/256)^n]n = 第 n 次窗更新);延迟 = 首个满足 CAPVD_n ≥ 判据 的 (n+1)·T_win',
], '#0b6')
note(750, 912, 740, 62, '注', [
'未计输出级 50 ms 主节拍量化(实际继电器动作再加 ≤50 ms);两型号均为单次越线判定(无进入/离开确认)。',
], '#0b6')
add('</svg>')
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d">'
'<rect width="%d" height="%d" fill="#fff"/>' % (W, H, W, H, W, H)) + ''.join(o)
out = 'docs/img/response-compare-pd132t-pd136ha.svg'
open(out, 'w', encoding='utf-8').write(svg)
print('SVG:', out, len(svg), 'bytes')
print('PD132T : tw=%.3f ms tau=%.1f ms(口径%.1f) 档1=%.4f%%' % (PT['tw'] * 1e3, PT['tau'] * 1e3, PT['tau_pl'] * 1e3, PT['thr'] * 100))
print('PD136HA: tw=%.3f ms tau=%.1f ms(口径%.1f) 档1=%.4f%%' % (PH['tw'] * 1e3, PH['tau'] * 1e3, PH['tau_pl'] * 1e3, PH['thr'] * 100))
print('+0.50%%: PD132T %.1f ms(第%d窗) | PD136HA %.1f ms(第%d窗)' % (msT5, iT5 + 1, msH5, iH5 + 1))
print('+0.15%%: PD132T %.1f ms(第%d窗) | PD136HA %.1f ms(第%d窗)' % (msT15, iT15 + 1, msH15, iH15 + 1))
print('+0.10%%: PD132T %.1f ms | PD136HA %.1f ms' % (delay_ms(PT, 0.001)[1] or -1, delay_ms(PH, 0.001)[1] or -1))