b46e44d92c
两份工作互锁(互相引用),故同一笔提交。 数据可信:全文按「五级台阶」重排(§1 全局 → §2 信度现状 → §3 M4 退出判据逐条 → §4 设计约束 → §5 任务卡 → §6 顺序 → §7 决策 → §8 风险 → §9 边界 → §10 关系)。 恢复 §2.2 量纲错配(该节此前被瘦身删掉,test/host/prot_dim 的两处引用一直悬空); 新增 §2.7 与 §4.6:ADC 只验过直流静态标定,正弦动态波形一次也没采过——用信号发生 器在既有注入点注 ±2 V 以内的正弦即可验全周傅氏、测频、快照与削顶路由,不必往端子 加压、也不需要标准源。§6 手绘框图改 Mermaid。章节编号未重排:§2 / §4 / §7 / §10 被 App 与 test 的头注释当规范引用。 故障录波:用户裁定 2026-10-29 录波先行,三条口径——1 kHz(每周波 20 点)、8 路 电气量全录、一屏只画一路共 8 路可切换。新增设计说明(与产品需求 80 点/周波口径的 偏离显式登记;存原始码值而非工程量,绕开未标定闭锁与测频幅值门;四个所有者的跨 任务边界与三条并发陷阱;段文件字节布局;单通道回放显示契约;约 5.9 KB 静态增量 触发 R6)与卡片登记簿(数据面最小闭环 → 手动与保护启动触发 → 现场单通道回放页, 二期的 4 kHz、突变量启动、COMTRADE、104 文件传输不返工的结构约束写进设计文档)。 两张旧配图与一张新配图改由 tools/arch_diagram/ 的生成器产出,脚本与产物同笔提交。 代码一行未动:本波只交付文档与设计。
251 lines
9.8 KiB
Python
251 lines
9.8 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""生成 docs/images/数据可信台阶.svg —— 「数据可信」的五级台阶:现在踩在第 2 级,
|
||
第 3 级(动态正弦波形)用手边设备就能跨(纯标准库)。
|
||
|
||
为什么不用 Mermaid:本图要读的是"台阶的高度差 + 每一级踩没踩上去"——Mermaid 会把
|
||
五级压成同一高度,"还差一级"这件事就看不出来了。谁挡谁的顺序拓扑另有一张 Mermaid
|
||
图,在 docs/任务规划/数据可信.md §6(本图刻意不重复那张的语义)。
|
||
|
||
用法(仓库根目录,任意 python3):
|
||
python tools/arch_diagram/gen_data_trust_ladder_svg.py
|
||
输出:
|
||
docs/images/数据可信台阶.svg
|
||
改内容:台阶事实(状态 / 判据 / 资源)变了才重画;调坐标 / 文案编辑下方常量后重跑。
|
||
"""
|
||
import html
|
||
import os
|
||
import sys
|
||
import xml.etree.ElementTree as ET
|
||
|
||
|
||
def emit(s):
|
||
"""控制台打印按目标编码替换,绝不因 GBK 终端遇上 ²/− 之类字符而崩
|
||
(generated-fresh 钩子会把本脚本拷进临时目录重跑,崩了就是一片假红)。"""
|
||
enc = getattr(sys.stdout, "encoding", None) or "ascii"
|
||
sys.stdout.write(s.encode(enc, "replace").decode(enc, "replace") + "\n")
|
||
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
OUT = os.path.join(ROOT, "docs", "images", "数据可信台阶.svg")
|
||
|
||
# ----------------------------- 画布与尺寸 -----------------------------
|
||
W = 1060
|
||
H = 712
|
||
PAD = 46
|
||
RIGHT = W - PAD
|
||
|
||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||
|
||
TITLE_FONT = 21
|
||
SUB_FONT = 13
|
||
HEAD_FONT = 14.5
|
||
Q_FONT = 12.5
|
||
HOW_FONT = 11.5
|
||
TINY_FONT = 11
|
||
|
||
INK = "#0f172a"
|
||
LINE = "#334155"
|
||
MUTED = "#64748b"
|
||
FAINT = "#94a3b8"
|
||
RULE = "#cbd5e1"
|
||
WHITE = "#ffffff"
|
||
|
||
C_GREEN = "#16a34a"
|
||
BG_GREEN = "#f0fdf4"
|
||
C_AMBER = "#d97706"
|
||
BG_AMBER = "#fffbeb"
|
||
C_ROSE = "#e11d48"
|
||
BG_ROSE = "#fff1f2"
|
||
C_BLUE = "#2563eb"
|
||
BG_BLUE = "#eff6ff"
|
||
|
||
# ----------------------------- 数据(改这里) -----------------------------
|
||
TITLE = "「数据可信」是五级台阶 —— 现在踩在第 2 级"
|
||
SUB1 = ("每升一级只多答一个问题:通路活 → 静态读数对 → 动态波形对 → 换算到端子 → 精度达标;"
|
||
"台阶的高度差 = 下游能多信多少")
|
||
SUB2 = ("第 3 级不要标准源、也不要往端子加压:一台信号发生器 + 已有的注入点就能做"
|
||
"(判据见 docs/任务规划/数据可信.md §4.6)")
|
||
|
||
# 每列:(编号, 台阶名, 状态, 主色, 底色, 这一级多答的问题, 用什么证 / 什么挡路)
|
||
STEPS = [
|
||
("1", "通路活", "已证 · 2026-10-27", C_GREEN, BG_GREEN,
|
||
"9 路都在转换?没串道?极性没反?",
|
||
"开机自检 + 帧计数仍 1000 Hz;逐路注入只动本路"),
|
||
("2", "静态引脚电压", "已证 · 2026-10-27", C_GREEN, BG_GREEN,
|
||
"注 1 V 直流,读数是不是 1 V?",
|
||
"万用表 + 电位器三点线性:斜率 6544.2 vs 理论 6553.6 码/V,差 −0.14 %"),
|
||
("3", "动态引脚波形", "待做 · 本文新增", C_ROSE, BG_ROSE,
|
||
"注 1 V 正弦,算出来还是不是 1 V?",
|
||
"注正弦验全周傅氏幅值 / 相位 / 测频 / 削顶 / 谐波抑制。"
|
||
"挡路:FREQ_AMP_MIN = 50 V 在引脚域恒不过 → 测频只能读到 IV"),
|
||
("4", "端子二次值", "等外部资源", C_AMBER, BG_AMBER,
|
||
"引脚上这 1 V 对应端子侧几 V?",
|
||
"④ 子板 / 端子板变比:仓内无原理图,须拿图纸或万用表实测"),
|
||
("5", "精度等级验收", "等外部资源", C_AMBER, BG_AMBER,
|
||
"额定工况下整条链误差够几级?",
|
||
"标准源两点标定写 [adc_cal] 置 VALID → 保护自动解卡 → 送检"),
|
||
]
|
||
|
||
BAND_LABEL_1 = "M4 承诺边界(§2.6):蓝底这三列都还在「ADC 引脚」这一侧,eng = 0、单位是引脚电压 V"
|
||
GATE_LABEL = "eng 升档门:④ 变比到手 + 电气 8 路 VALID 全置位 → eng 才升 1"
|
||
FOOTNOTE = ("第 2、3 级共用同一个注入点与同一套读数出口,但证的是两件事:"
|
||
"直流证「一条水平线的高度」,正弦证「整条时间轴的形状与节拍」——第 3 级不能由第 2 级代证。")
|
||
|
||
# ----------------------------- 布局 -----------------------------
|
||
STEP_W = 196
|
||
STEP_GAP = 6
|
||
X0 = PAD
|
||
FIRST_H = 96 # 第 1 级台阶高度
|
||
RISE = 64 # 每高一级加这么多
|
||
BASE_Y = 466 # 地面线
|
||
GATE_X = X0 + 3 * (STEP_W + STEP_GAP) - STEP_GAP - 3 # 第 3/4 列之间
|
||
BAND_TOP = 106
|
||
BAND_BOTTOM = BASE_Y + 172
|
||
|
||
|
||
# ----------------------------- 工具函数 -----------------------------
|
||
def esc(s):
|
||
return html.escape(s, quote=True)
|
||
|
||
|
||
def text_w(s, fs):
|
||
"""粗略估算文本像素宽:CJK/全角 ≈ 1.0em,其余 ≈ 0.58em(宁可偏大)。"""
|
||
return sum(fs * (1.0 if ord(ch) >= 0x2E80 else 0.58) for ch in s)
|
||
|
||
|
||
OVERFLOW = []
|
||
|
||
|
||
def check_fit(s, fs, avail, where):
|
||
if text_w(s, fs) > avail:
|
||
OVERFLOW.append("%s: %.0f > %.0f [%s]" % (where, text_w(s, fs), avail, s))
|
||
|
||
|
||
def txt(x, y, s, fs, fill, weight=400, anchor=None):
|
||
a = "" if anchor is None else ' text-anchor="%s"' % anchor
|
||
w = "" if weight == 400 else ' font-weight="%d"' % weight
|
||
return ('<text x="%.1f" y="%.1f" font-size="%g"%s%s fill="%s">%s</text>'
|
||
% (x, y, fs, a, w, fill, esc(s)))
|
||
|
||
|
||
def line(x1, y1, x2, y2, color=LINE, width=2.0, dash=None):
|
||
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
|
||
return ('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%g"%s/>'
|
||
% (x1, y1, x2, y2, color, width, d))
|
||
|
||
|
||
def rect(x, y, w, h, fill, stroke=None, rx=8, sw=1.6):
|
||
s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw)
|
||
return '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%g" fill="%s"%s/>' % (
|
||
x, y, w, h, rx, fill, s)
|
||
|
||
|
||
def wrap(s, fs, avail, max_lines, where=""):
|
||
"""按估算宽度折行(CJK 逐字可断)。超出 max_lines 用省略号收尾——
|
||
宁可提示文案太长,也不让字溢出盒子。"""
|
||
if text_w(s, fs) <= avail:
|
||
return [s]
|
||
lines = []
|
||
cur = ""
|
||
for ch in s:
|
||
if text_w(cur + ch, fs) > avail:
|
||
lines.append(cur)
|
||
cur = ch
|
||
else:
|
||
cur += ch
|
||
if cur:
|
||
lines.append(cur)
|
||
if len(lines) > max_lines:
|
||
OVERFLOW.append("%s: 折行 %d > %d" % (where, len(lines), max_lines))
|
||
lines = lines[:max_lines]
|
||
lines[max_lines - 1] = lines[max_lines - 1][:-1] + "…"
|
||
return lines
|
||
|
||
|
||
# ----------------------------- 生成 -----------------------------
|
||
def build():
|
||
out = []
|
||
add = out.append
|
||
|
||
avail = RIGHT - PAD
|
||
for s, fs, where in ((TITLE, TITLE_FONT, "title"), (SUB1, SUB_FONT, "sub1"),
|
||
(SUB2, SUB_FONT, "sub2")):
|
||
check_fit(s, fs, avail, where)
|
||
if OVERFLOW:
|
||
for o in OVERFLOW:
|
||
emit("OVERFLOW WARN " + o)
|
||
|
||
add('<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' % (W, H, WHITE))
|
||
|
||
# ---- 标题区 ----
|
||
add(txt(PAD, 42, TITLE, TITLE_FONT, INK, weight=700))
|
||
add(txt(PAD, 68, SUB1, SUB_FONT, MUTED))
|
||
add(txt(PAD, 88, SUB2, SUB_FONT, MUTED))
|
||
|
||
# ---- 「引脚域」底衬(先画,压在台阶下面)----
|
||
add(rect(PAD - 12, BAND_TOP, GATE_X - PAD + 3, BAND_BOTTOM - BAND_TOP, BG_BLUE, None, rx=12))
|
||
|
||
# ---- 台阶 ----
|
||
inner_w = STEP_W - 20
|
||
for i, (no, name, status, c, bg, q, how) in enumerate(STEPS):
|
||
x = X0 + i * (STEP_W + STEP_GAP)
|
||
h = FIRST_H + i * RISE
|
||
top = BASE_Y - h
|
||
add(rect(x, top, STEP_W, h, bg, c, rx=8, sw=2.0))
|
||
add('<circle cx="%.1f" cy="%.1f" r="13" fill="%s"/>' % (x + 24, top + 26, c))
|
||
add(txt(x + 24, top + 31, no, 13, WHITE, weight=700, anchor="middle"))
|
||
check_fit(name, HEAD_FONT, inner_w - 30, "step %s name" % no)
|
||
add(txt(x + 44, top + 32, name, HEAD_FONT, INK, weight=700))
|
||
add(txt(x + 11, top + 58, status, TINY_FONT, c, weight=700))
|
||
|
||
# ---- 地面线 ----
|
||
add(line(PAD, BASE_Y, RIGHT, BASE_Y, LINE, 2.2))
|
||
|
||
# ---- 地面下的明细(问题 + 用什么证),逐列一行 ----
|
||
for i, (no, name, status, c, bg, q, how) in enumerate(STEPS):
|
||
x = X0 + i * (STEP_W + STEP_GAP)
|
||
y = BASE_Y + 24
|
||
for ln in wrap(q, Q_FONT, inner_w, 2, "step %s q" % no):
|
||
add(txt(x + 10, y, ln, Q_FONT, INK, weight=700))
|
||
y += 17
|
||
y += 3
|
||
for ln in wrap(how, HOW_FONT, inner_w, 5, "step %s how" % no):
|
||
add(txt(x + 10, y, ln, HOW_FONT, MUTED))
|
||
y += 15
|
||
|
||
# ---- eng 升档门 ----
|
||
add(line(GATE_X, BAND_TOP - 6, GATE_X, BAND_BOTTOM, C_ROSE, 2.0, dash="7 5"))
|
||
gy = BAND_TOP + 12
|
||
for ln in wrap(GATE_LABEL, TINY_FONT, STEP_W - 16, 4, "gate label"):
|
||
add(txt(GATE_X + 8, gy, ln, TINY_FONT, C_ROSE, weight=700))
|
||
gy += 15
|
||
|
||
# ---- 蓝底标签(在台阶上方的空白区)----
|
||
bl = wrap(BAND_LABEL_1, HOW_FONT, GATE_X - PAD - 24, 2, "band label")
|
||
for i, ln in enumerate(bl):
|
||
add(txt(PAD, BAND_TOP + 26 + i * 17, ln, HOW_FONT, C_BLUE, weight=700))
|
||
|
||
# ---- 底部说明 ----
|
||
fy = BAND_BOTTOM + 22
|
||
add(line(PAD, fy, RIGHT, fy, RULE, 1.0))
|
||
for i, ln in enumerate(wrap(FOOTNOTE, SUB_FONT, avail, 2, "foot wrap")):
|
||
add(txt(PAD, fy + 24 + i * 19, ln, SUB_FONT, MUTED))
|
||
|
||
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
|
||
'viewBox="0 0 %d %d" font-family="%s">\n%s\n</svg>\n'
|
||
% (W, H, W, H, FONT, "\n".join(out)))
|
||
|
||
d = os.path.dirname(OUT)
|
||
if not os.path.isdir(d):
|
||
os.makedirs(d)
|
||
with open(OUT, "w", encoding="utf-8", newline="\n") as fp:
|
||
fp.write(svg)
|
||
ET.fromstring(svg)
|
||
emit("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, H, OUT))
|
||
if OVERFLOW:
|
||
emit("!! %d overflow warnings" % len(OVERFLOW))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
build()
|