425 lines
18 KiB
Python
425 lines
18 KiB
Python
#!/usr/bin/env python3
|
||||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""生成对接电机积分差归因 HTML(通俗说明 + 标注图)。"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import json
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
LAB = Path(__file__).resolve().parents[1] / "lab"
|
|||
|
|
SRC = LAB / "ecat_who_slow.json"
|
|||
|
|
OUT = LAB / "ecat_integral_report.html"
|
|||
|
|
|
|||
|
|
data = json.loads(SRC.read_text(encoding="utf-8"))
|
|||
|
|
pa = data["phase_attribution"]
|
|||
|
|
strokes = data["strokes"]
|
|||
|
|
sample = data.get("plot_sample") or []
|
|||
|
|
verdict = data.get("verdict") or {}
|
|||
|
|
|
|||
|
|
# 用行程数据修正通俗结论(避免停车检测为 0 的误导)
|
|||
|
|
on_vals = pa["on_ms_n3_minus_n2"]["values"]
|
|||
|
|
# 剔除 |Δ|>50ms 的伪起动(上一段未停稳造成的重叠)
|
|||
|
|
on_clean = [v for v in on_vals if abs(v) <= 50]
|
|||
|
|
on_overlap = [v for v in on_vals if abs(v) > 50]
|
|||
|
|
I_on = [s["I_on"]["Isum"] for s in strokes]
|
|||
|
|
I_cr = [s["I_cruise"]["Isum"] for s in strokes]
|
|||
|
|
I_st = [s["I_stop"]["Isum"] for s in strokes]
|
|||
|
|
only2 = [s["stop_only_n2"]["Isum"] for s in strokes]
|
|||
|
|
only3 = [s["stop_only_n3"]["Isum"] for s in strokes]
|
|||
|
|
# 停止窗谁残速大:I_st>0 → 合速度偏正 → 镜像下更像 N3 仍正/N2 已减
|
|||
|
|
# 直接用 only_* 贡献绝对值
|
|||
|
|
sum_only2 = sum(abs(x) for x in only2)
|
|||
|
|
sum_only3 = sum(abs(x) for x in only3)
|
|||
|
|
n_only2 = sum(1 for x in only2 if abs(x) > 0.2)
|
|||
|
|
n_only3 = sum(1 for x in only3 if abs(x) > 0.2)
|
|||
|
|
|
|||
|
|
cruise_v2 = pa["cruise_mean_abs_v2"]["mean"]
|
|||
|
|
cruise_v3 = pa["cruise_mean_abs_v3"]["mean"]
|
|||
|
|
cruise_bias = pa["cruise_bias_v2_minus_v3"]["mean"]
|
|||
|
|
|
|||
|
|
mean_on_clean = sum(on_clean) / len(on_clean) if on_clean else None
|
|||
|
|
mean_Ion = sum(I_on) / len(I_on) if I_on else 0
|
|||
|
|
mean_Icr = sum(I_cr) / len(I_cr) if I_cr else 0
|
|||
|
|
mean_Ist = sum(I_st) / len(I_st) if I_st else 0
|
|||
|
|
abs_Ist = sum(abs(x) for x in I_st) / len(I_st) if I_st else 0
|
|||
|
|
abs_Ion = sum(abs(x) for x in I_on) / len(I_on) if I_on else 0
|
|||
|
|
abs_Icr = sum(abs(x) for x in I_cr) / len(I_cr) if I_cr else 0
|
|||
|
|
|
|||
|
|
# SVG 折线
|
|||
|
|
W, H = 960, 420
|
|||
|
|
PAD_L, PAD_R, PAD_T = 56, 20, 36
|
|||
|
|
|
|||
|
|
|
|||
|
|
def poly(xs, ys, y0, y1, top, h, color, w=1.3):
|
|||
|
|
if not xs:
|
|||
|
|
return ""
|
|||
|
|
lo, hi = y0, y1
|
|||
|
|
if hi - lo < 1e-9:
|
|||
|
|
hi = lo + 1
|
|||
|
|
|
|||
|
|
def X(t, t0, t1):
|
|||
|
|
span = max(t1 - t0, 1e-6)
|
|||
|
|
return PAD_L + (t - t0) / span * (W - PAD_L - PAD_R)
|
|||
|
|
|
|||
|
|
def Y(v):
|
|||
|
|
return top + (hi - v) / (hi - lo) * h
|
|||
|
|
|
|||
|
|
pts = " ".join(f"{X(xs[i], xs[0], xs[-1]):.1f},{Y(ys[i]):.1f}" for i in range(len(xs)))
|
|||
|
|
return f'<polyline fill="none" stroke="{color}" stroke-width="{w}" points="{pts}"/>'
|
|||
|
|
|
|||
|
|
|
|||
|
|
if sample:
|
|||
|
|
ts = [p["t"] for p in sample]
|
|||
|
|
v2 = [p["v2"] for p in sample]
|
|||
|
|
v3 = [p["v3"] for p in sample]
|
|||
|
|
vsum = [p["vsum"] for p in sample]
|
|||
|
|
Isum = [p["Isum"] for p in sample]
|
|||
|
|
t0, t1 = ts[0], ts[-1]
|
|||
|
|
vmin = min(min(v2), min(v3))
|
|||
|
|
vmax = max(max(v2), max(v3))
|
|||
|
|
imin = min(min(Isum), 0)
|
|||
|
|
imax = max(max(Isum), 0)
|
|||
|
|
padv = (vmax - vmin) * 0.08 or 10
|
|||
|
|
padI = (imax - imin) * 0.1 or 2
|
|||
|
|
|
|||
|
|
def X(t):
|
|||
|
|
return PAD_L + (t - t0) / max(t1 - t0, 1e-6) * (W - PAD_L - PAD_R)
|
|||
|
|
|
|||
|
|
def Yv(v):
|
|||
|
|
return 48 + (vmax + padv - v) / (vmax - vmin + 2 * padv) * 100
|
|||
|
|
|
|||
|
|
def Ys(v):
|
|||
|
|
return 190 + (vmax + padv - v) / (vmax - vmin + 2 * padv) * 80
|
|||
|
|
|
|||
|
|
def YI(v):
|
|||
|
|
return 300 + (imax + padI - v) / (imax - imin + 2 * padI) * 80
|
|||
|
|
|
|||
|
|
def line(vals, yfn, color, w=1.3):
|
|||
|
|
pts = " ".join(f"{X(ts[i]):.1f},{yfn(vals[i]):.1f}" for i in range(len(ts)))
|
|||
|
|
return f'<polyline fill="none" stroke="{color}" stroke-width="{w}" points="{pts}"/>'
|
|||
|
|
|
|||
|
|
# 行程分界竖线
|
|||
|
|
vlines = []
|
|||
|
|
for sm in data.get("stroke_marks") or []:
|
|||
|
|
if sm["t0"] <= t1:
|
|||
|
|
x = X(sm["t0"])
|
|||
|
|
vlines.append(
|
|||
|
|
f'<line x1="{x:.1f}" x2="{x:.1f}" y1="40" y2="390" stroke="#2a3340" stroke-width="1"/>'
|
|||
|
|
)
|
|||
|
|
vlines.append(
|
|||
|
|
f'<text x="{x+3:.1f}" y="54" fill="#5c6b7e" font-size="9" '
|
|||
|
|
f'font-family="Segoe UI,sans-serif">#{sm["i"]}</text>'
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
ts = v2 = v3 = vsum = Isum = []
|
|||
|
|
t0 = t1 = 0
|
|||
|
|
vlines = []
|
|||
|
|
|
|||
|
|
# 标注箭头:在图上用文字+简单箭头路径
|
|||
|
|
notes_svg = []
|
|||
|
|
if sample:
|
|||
|
|
# 找几个典型点
|
|||
|
|
# 1) 巡航:|v| 稳定处
|
|||
|
|
# 2) 停车:vsum 尖峰
|
|||
|
|
peak_i = max(range(len(sample)), key=lambda i: abs(sample[i]["vsum"]))
|
|||
|
|
# 理论零线
|
|||
|
|
notes_svg.append(
|
|||
|
|
f'<text x="{PAD_L}" y="28" fill="#e8eef7" font-size="14" '
|
|||
|
|
f'font-family="Segoe UI,Microsoft YaHei,sans-serif">'
|
|||
|
|
f'对接电机 2/3:速度、合速度 v2+v3、积分 ∫(v2+v3)dt(虚线为理论 0)</text>'
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 表格行
|
|||
|
|
rows_html = []
|
|||
|
|
for s in strokes:
|
|||
|
|
cls_on = "bad" if abs(s["on_ms_n3_minus_n2"]) > 50 else "ok"
|
|||
|
|
cls_cr = "ok"
|
|||
|
|
cls_st = "warn" if abs(s["I_stop"]["Isum"]) > 3 else "ok"
|
|||
|
|
who = ""
|
|||
|
|
if abs(s["stop_only_n3"]["Isum"]) > abs(s["stop_only_n2"]["Isum"]) and abs(s["stop_only_n3"]["Isum"]) > 0.2:
|
|||
|
|
who = "N3 残速"
|
|||
|
|
elif abs(s["stop_only_n2"]["Isum"]) > 0.2:
|
|||
|
|
who = "N2 残速"
|
|||
|
|
else:
|
|||
|
|
who = "两台接近同步刹停"
|
|||
|
|
rows_html.append(f"""
|
|||
|
|
<tr>
|
|||
|
|
<td>#{s['i']}</td><td>{s['dir_n2']}</td>
|
|||
|
|
<td class="{cls_on}">{s['on_ms_n3_minus_n2']:+.2f} ms</td>
|
|||
|
|
<td class="{cls_cr}">{s['cruise']['mean_abs_v2']:.2f} / {s['cruise']['mean_abs_v3']:.2f}</td>
|
|||
|
|
<td>{s['I_on']['Isum']:+.2f}</td>
|
|||
|
|
<td class="ok">{s['I_cruise']['Isum']:+.2f}</td>
|
|||
|
|
<td class="{cls_st}">{s['I_stop']['Isum']:+.2f}</td>
|
|||
|
|
<td>{s['stop_only_n2']['Isum']:+.2f}</td>
|
|||
|
|
<td>{s['stop_only_n3']['Isum']:+.2f}</td>
|
|||
|
|
<td class="{cls_st}">{who}</td>
|
|||
|
|
<td>{s['da2_plus_da3']:+.2f}°</td>
|
|||
|
|
</tr>""")
|
|||
|
|
|
|||
|
|
# 通俗结论文本
|
|||
|
|
if abs_Ist > abs_Icr * 3 and abs_Ist > abs_Ion:
|
|||
|
|
main_src = "停车阶段"
|
|||
|
|
elif abs_Ion > abs_Ist and abs_Ion > abs_Icr:
|
|||
|
|
main_src = "行程衔接/起动窗口(含上一段未停稳的重叠)"
|
|||
|
|
else:
|
|||
|
|
main_src = "起动与停车都有贡献,巡航最干净"
|
|||
|
|
|
|||
|
|
plain_html = f"""
|
|||
|
|
<section class="card">
|
|||
|
|
<h2>通俗结论:到底是谁慢?</h2>
|
|||
|
|
<p class="lead">先说人话:<strong>两台电机并不是“有一台全程都慢”</strong>。
|
|||
|
|
你看到的积分差,主要不是巡航时谁转得慢,而是<strong>快停下 / 刚要走的那几十一到几百毫秒里,两台对不齐</strong>,
|
|||
|
|
把理论应为 0 的 <code>∫(v2+v3)dt</code> 顶出 5~20° 左右的偏差。</p>
|
|||
|
|
|
|||
|
|
<ul class="verdict">
|
|||
|
|
<li><span class="tag ok">启动</span>
|
|||
|
|
干净起动时(|Δt|≤50ms 的行程),N3−N2 约为
|
|||
|
|
<strong>{mean_on_clean if mean_on_clean is not None else '—'} ms</strong> 量级 →
|
|||
|
|
<strong>几乎是同时启动</strong>,ECAT 同帧下发没有问题。
|
|||
|
|
有 {len(on_overlap)} 个行程测出上百毫秒的“假早启动”,那是<strong>上一段还没停稳、N3 仍在动</strong>,
|
|||
|
|
不是这一段命令发晚了。</li>
|
|||
|
|
<li><span class="tag ok">巡航</span>
|
|||
|
|
两边平均 |v|:N2 <strong>{cruise_v2:.2f}</strong>°/s,N3 <strong>{cruise_v3:.2f}</strong>°/s,
|
|||
|
|
差值约 <strong>{cruise_bias:.3f}</strong>°/s → <strong>稳态速度几乎一样,谈不上谁慢</strong>。
|
|||
|
|
巡航段 ∫(v2+v3) 平均只有约 <strong>{mean_Icr:.2f}°</strong>,贴合理论 0。</li>
|
|||
|
|
<li><span class="tag warn">停车 / 换向</span>
|
|||
|
|
行程末端 ∫(v2+v3) 平均约 <strong>{mean_Ist:.2f}°</strong>,绝对值平均约 <strong>{abs_Ist:.2f}°</strong>,
|
|||
|
|
明显大于巡航。停止窗口里:
|
|||
|
|
出现「只有 N2 还在走」的行程约 <strong>{n_only2}</strong> 次(积分贡献合计 {sum_only2:.2f}°),
|
|||
|
|
「只有 N3 还在走」约 <strong>{n_only3}</strong> 次(合计 {sum_only3:.2f}°)。
|
|||
|
|
<strong>两边都有刹车尾巴,不是单边永久变慢</strong>;有的行程 N2 拖尾更明显,有的行程 N3 更明显。</li>
|
|||
|
|
</ul>
|
|||
|
|
|
|||
|
|
<div class="callout">
|
|||
|
|
<strong>一句话:</strong>积分差的大头在 <strong>{main_src}</strong>。
|
|||
|
|
对接约束要求 <code>v2 = −v3</code>;只要刹停瞬间一台还带着速度、另一台已经接近 0,
|
|||
|
|
<code>v2+v3</code> 就会冒尖峰,积分就会被拉离理论 0。这属于<strong>动态不同步</strong>,
|
|||
|
|
不等于某台电机“标定转速低、一直慢”。
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="arrow-legend">
|
|||
|
|
<div><span class="arrow ok-arrow"></span> 正常:同时启动 / 巡航一致 / 中段积分≈0</div>
|
|||
|
|
<div><span class="arrow warn-arrow"></span> 注意:停车沿残速、行程衔接重叠、积分被顶开</div>
|
|||
|
|
<div><span class="arrow bad-arrow"></span> 偏大:该行程 ∫(v2+v3) 或编码器 Δ(a2+a3) 明显偏离 0</div>
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
rows_joined = "\n".join(rows_html)
|
|||
|
|
|
|||
|
|
# 标注框(放在 SVG 下方用 HTML 箭头说明)
|
|||
|
|
annot_html = f"""
|
|||
|
|
<section class="card">
|
|||
|
|
<h2>图上怎么看(对照上方曲线)</h2>
|
|||
|
|
<div class="annot-grid">
|
|||
|
|
<div class="annot ok">
|
|||
|
|
<div class="ann-title">↑ 巡航段(两条速度曲线几乎贴合)</div>
|
|||
|
|
<p>N2 蓝线、N3 橙线在 |v|≈200°/s 附近重合 → <strong>都不慢</strong>。
|
|||
|
|
下方红线 <code>v2+v3</code> 贴 0,积分曲线几乎水平 → 对接约束在稳态是成立的。</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="annot warn">
|
|||
|
|
<div class="ann-title">↓ 停车沿 / 换向点(红线尖峰)</div>
|
|||
|
|
<p>接近终点时,一台速度先掉下来、另一台还在走 → <code>v2+v3</code> 出现 ±100~200°/s 尖峰。
|
|||
|
|
积分曲线在这里<strong>爬上或爬下</strong>,行程结束就停在理论 0 以外几度到二十度。
|
|||
|
|
<strong>这里是积分差的主要来源。</strong></p>
|
|||
|
|
</div>
|
|||
|
|
<div class="annot warn">
|
|||
|
|
<div class="ann-title">↓ 行程刚衔接时(起动窗)</div>
|
|||
|
|
<p>若上一段没完全停稳就反向,会看到“N3 早就动了”的假象(表格里 onΔ 为 −800ms 量级)。
|
|||
|
|
这<strong>不是启动指令不同步</strong>,而是<strong>速度还在反向爬坡 / 残速</strong>,
|
|||
|
|
也会把起动窗积分抬高一点。</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="annot bad">
|
|||
|
|
<div class="ann-title">↓ 紫色角度和 a2+a3</div>
|
|||
|
|
<p>理论应为常数(理想为 0)。行程终点若偏到 ±10~25°,与该行程 ∫(v2+v3) 同量级 →
|
|||
|
|
说明偏差是<strong>真实位移耦合误差</strong>,不只是速度噪声。</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="callout small">
|
|||
|
|
<strong>标定建议(若要压积分差):</strong>
|
|||
|
|
① 先统一两台的减速段(同样斜率刹停),而不是去改巡航速度;
|
|||
|
|
② 检查 N2/N3 负载与制动是否对称;
|
|||
|
|
③ 控制器上对对接轴做 <code>v2+v3 → 0</code> 的纠偏,比单轴调 PID 更对症;
|
|||
|
|
④ 序列 hold 不要短于“两台都确认停稳”的时间,避免重叠行程把积分搅浑。
|
|||
|
|
</div>
|
|||
|
|
</section>
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# SVG 图
|
|||
|
|
if sample:
|
|||
|
|
svg = f"""
|
|||
|
|
<svg viewBox="0 0 {W} {H}" width="100%" xmlns="http://www.w3.org/2000/svg">
|
|||
|
|
<rect width="{W}" height="{H}" fill="#0f1419"/>
|
|||
|
|
{''.join(vlines)}
|
|||
|
|
{line(v2, Yv, '#4da3ff')}
|
|||
|
|
{line(v3, Yv, '#f0a030')}
|
|||
|
|
<text x="{PAD_L}" y="44" fill="#8a9bb0" font-size="11" font-family="Segoe UI,Microsoft YaHei,sans-serif">速度 deg/s — 蓝 N2 / 橙 N3</text>
|
|||
|
|
<line x1="{PAD_L}" x2="{W-PAD_R}" y1="{Yv(0):.1f}" y2="{Yv(0):.1f}" stroke="#2a3340" stroke-dasharray="4 3"/>
|
|||
|
|
{line(vsum, Ys, '#ff5c5c')}
|
|||
|
|
<text x="{PAD_L}" y="184" fill="#8a9bb0" font-size="11" font-family="Segoe UI,Microsoft YaHei,sans-serif">合速度 v2+v3(理论 0)— 停车尖峰在这里</text>
|
|||
|
|
<line x1="{PAD_L}" x2="{W-PAD_R}" y1="{Ys(0):.1f}" y2="{Ys(0):.1f}" stroke="#3a4656" stroke-dasharray="4 3"/>
|
|||
|
|
{line(Isum, YI, '#b48cff')}
|
|||
|
|
<text x="{PAD_L}" y="294" fill="#8a9bb0" font-size="11" font-family="Segoe UI,Microsoft YaHei,sans-serif">积分 ∫(v2+v3)dt(理论 0)— 每段末的偏离就是耦合误差</text>
|
|||
|
|
<line x1="{PAD_L}" x2="{W-PAD_R}" y1="{YI(0):.1f}" y2="{YI(0):.1f}" stroke="#3a4656" stroke-dasharray="4 3"/>
|
|||
|
|
<text x="{PAD_L}" y="{H-12}" fill="#6b7a8d" font-size="11" font-family="Segoe UI,sans-serif">t_rel {t0:.1f}..{t1:.1f}s · ECAT 0.25ms · 竖线为行程起点</text>
|
|||
|
|
<!-- 标注箭头 -->
|
|||
|
|
<g font-family="Segoe UI,Microsoft YaHei,sans-serif" font-size="11">
|
|||
|
|
<text x="{W-210}" y="70" fill="#3d8f5b">↑ 巡航:两线贴合,都不慢</text>
|
|||
|
|
<text x="{W-230}" y="210" fill="#d4a017">↓ 停车沿:v_sum 尖峰 → 积分爬升</text>
|
|||
|
|
</g>
|
|||
|
|
</svg>"""
|
|||
|
|
else:
|
|||
|
|
svg = "<p>(无绘图样本)</p>"
|
|||
|
|
|
|||
|
|
html = f"""<!DOCTYPE html>
|
|||
|
|
<html lang="zh-CN">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="utf-8"/>
|
|||
|
|
<title>对接电机 2/3 积分差归因报告</title>
|
|||
|
|
<style>
|
|||
|
|
:root {{
|
|||
|
|
--bg:#0f1419; --card:#161c24; --ink:#e8eef7; --mut:#8a9bb0;
|
|||
|
|
--ok:#3d8f5b; --warn:#d4a017; --bad:#e25555; --line:#2a3340;
|
|||
|
|
}}
|
|||
|
|
* {{ box-sizing:border-box; }}
|
|||
|
|
body {{
|
|||
|
|
margin:0; background:var(--bg); color:var(--ink);
|
|||
|
|
font-family:"Segoe UI","Microsoft YaHei",system-ui,sans-serif;
|
|||
|
|
line-height:1.55; padding:24px;
|
|||
|
|
}}
|
|||
|
|
h1 {{ font-size:22px; font-weight:600; margin:0 0 6px; }}
|
|||
|
|
h2 {{ font-size:16px; font-weight:600; margin:0 0 12px; }}
|
|||
|
|
.sub {{ color:var(--mut); font-size:13px; margin-bottom:18px; }}
|
|||
|
|
.card {{
|
|||
|
|
background:var(--card); border:1px solid var(--line); border-radius:12px;
|
|||
|
|
padding:18px 20px; margin-bottom:16px;
|
|||
|
|
}}
|
|||
|
|
.lead {{ margin:0 0 12px; color:var(--ink); }}
|
|||
|
|
code {{
|
|||
|
|
background:#0c1014; padding:1px 6px; border-radius:4px;
|
|||
|
|
font-family:Consolas,monospace; font-size:13px; color:#c5d4e8;
|
|||
|
|
}}
|
|||
|
|
ul.verdict {{ list-style:none; padding:0; margin:0 0 14px; }}
|
|||
|
|
ul.verdict li {{
|
|||
|
|
margin:0 0 10px; padding:10px 12px; background:#121820;
|
|||
|
|
border-left:3px solid var(--line); border-radius:0 8px 8px 0;
|
|||
|
|
}}
|
|||
|
|
.tag {{
|
|||
|
|
display:inline-block; font-size:12px; font-weight:600; padding:2px 8px;
|
|||
|
|
border-radius:999px; margin-right:8px; vertical-align:middle;
|
|||
|
|
}}
|
|||
|
|
.tag.ok {{ background:rgba(61,143,91,.2); color:#6dca8f; }}
|
|||
|
|
.tag.warn {{ background:rgba(212,160,23,.2); color:#f0c35a; }}
|
|||
|
|
.callout {{
|
|||
|
|
background:#121a14; border:1px solid #2a4a34; border-radius:8px;
|
|||
|
|
padding:12px 14px; margin-top:8px;
|
|||
|
|
}}
|
|||
|
|
.callout.small {{ font-size:13px; background:#1a1520; border-color:#3a2a4a; }}
|
|||
|
|
table {{
|
|||
|
|
width:100%; border-collapse:collapse; font-size:12.5px; margin-top:8px;
|
|||
|
|
}}
|
|||
|
|
th, td {{
|
|||
|
|
border-bottom:1px solid var(--line); padding:6px 8px; text-align:left;
|
|||
|
|
}}
|
|||
|
|
th {{ color:var(--mut); font-weight:500; }}
|
|||
|
|
td.ok {{ color:#6dca8f; }}
|
|||
|
|
td.warn {{ color:#f0c35a; }}
|
|||
|
|
td.bad {{ color:#ff8a8a; }}
|
|||
|
|
.annot-grid {{
|
|||
|
|
display:grid; grid-template-columns:1fr 1fr; gap:12px;
|
|||
|
|
}}
|
|||
|
|
@media (max-width:800px) {{ .annot-grid {{ grid-template-columns:1fr; }} }}
|
|||
|
|
.annot {{
|
|||
|
|
background:#121820; border-radius:8px; padding:12px; border-left:3px solid var(--line);
|
|||
|
|
}}
|
|||
|
|
.annot.ok {{ border-left-color:var(--ok); }}
|
|||
|
|
.annot.warn {{ border-left-color:var(--warn); }}
|
|||
|
|
.annot.bad {{ border-left-color:var(--bad); }}
|
|||
|
|
.ann-title {{ font-weight:600; margin-bottom:6px; font-size:13px; }}
|
|||
|
|
.annot p {{ margin:0; font-size:13px; color:#c5d0dc; }}
|
|||
|
|
.arrow-legend {{ margin-top:12px; font-size:13px; color:var(--mut); }}
|
|||
|
|
.arrow-legend div {{ margin:4px 0; }}
|
|||
|
|
.arrow {{ display:inline-block; width:18px; height:3px; margin-right:8px; vertical-align:middle; }}
|
|||
|
|
.ok-arrow {{ background:var(--ok); }}
|
|||
|
|
.warn-arrow {{ background:var(--warn); }}
|
|||
|
|
.bad-arrow {{ background:var(--bad); }}
|
|||
|
|
.metrics {{
|
|||
|
|
display:grid; grid-template-columns:repeat(4,1fr); gap:10px; margin-top:10px;
|
|||
|
|
}}
|
|||
|
|
@media (max-width:800px) {{ .metrics {{ grid-template-columns:1fr 1fr; }} }}
|
|||
|
|
.metric {{
|
|||
|
|
background:#121820; border:1px solid var(--line); border-radius:8px; padding:10px 12px;
|
|||
|
|
}}
|
|||
|
|
.metric .k {{ color:var(--mut); font-size:12px; }}
|
|||
|
|
.metric .v {{ font-size:20px; font-weight:600; margin-top:2px; }}
|
|||
|
|
.metric .v.ok {{ color:#6dca8f; }}
|
|||
|
|
.metric .v.warn {{ color:#f0c35a; }}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<h1>对接电机 2 / 3:积分差是谁造成的?</h1>
|
|||
|
|
<div class="sub">
|
|||
|
|
数据:ECAT 无源采集 <code>lab/ecat_passive_latest.csv</code> · 周期 0.25ms ·
|
|||
|
|
理论:刚性镜像对接时 <code>v2+v3≡0</code>,<code>∫(v2+v3)dt≡0</code>,<code>a2+a3=常数</code><br/>
|
|||
|
|
方法:对齐 cycles 后做梯形积分;先加后积与先积后加一致。
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card">
|
|||
|
|
<h2>关键数字</h2>
|
|||
|
|
<div class="metrics">
|
|||
|
|
<div class="metric"><div class="k">巡航 |v| N2 / N3</div>
|
|||
|
|
<div class="v ok">{cruise_v2:.2f} / {cruise_v3:.2f}</div></div>
|
|||
|
|
<div class="metric"><div class="k">巡航段 ∫(v2+v3) 平均</div>
|
|||
|
|
<div class="v ok">{mean_Icr:.2f}°</div></div>
|
|||
|
|
<div class="metric"><div class="k">停车段 ∫(v2+v3) |均值|</div>
|
|||
|
|
<div class="v warn">{abs_Ist:.2f}°</div></div>
|
|||
|
|
<div class="metric"><div class="k">干净起动 Δt 中位/均值</div>
|
|||
|
|
<div class="v ok">{(sorted(on_clean)[len(on_clean)//2] if on_clean else float('nan')):.2f} / {(mean_on_clean if mean_on_clean is not None else float('nan')):.2f} ms</div></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{plain_html}
|
|||
|
|
|
|||
|
|
<section class="card">
|
|||
|
|
<h2>曲线(带标注)</h2>
|
|||
|
|
{svg}
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
{annot_html}
|
|||
|
|
|
|||
|
|
<section class="card">
|
|||
|
|
<h2>各行程明细(积分从行程起点重算)</h2>
|
|||
|
|
<table>
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>#</th><th>方向</th><th>起动Δ(N3-N2)</th><th>巡航|v| N2/N3</th>
|
|||
|
|
<th>∫起动</th><th>∫巡航</th><th>∫停车</th>
|
|||
|
|
<th>停时仅N2在动</th><th>停时仅N3在动</th><th>停车归因</th><th>Δ(a2+a3)</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
{rows_joined}
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
<p class="sub" style="margin-top:10px">
|
|||
|
|
单位:角度 deg(积分)· 速度 deg/s · 时间 ms。
|
|||
|
|
「停时仅N2/N3在动」为该窗口内单边残速对 ∫(v2+v3) 的贡献。
|
|||
|
|
</p>
|
|||
|
|
</section>
|
|||
|
|
|
|||
|
|
<section class="card">
|
|||
|
|
<h2>原始结论脚本输出(备查)</h2>
|
|||
|
|
<pre style="white-space:pre-wrap;font-size:12px;color:#a8b6c8;margin:0">{json.dumps(verdict, ensure_ascii=False, indent=2)}</pre>
|
|||
|
|
</section>
|
|||
|
|
</body>
|
|||
|
|
</html>
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
OUT.write_text(html, encoding="utf-8")
|
|||
|
|
print("HTML ->", OUT)
|
|||
|
|
print("main_src:", main_src)
|
|||
|
|
print("on_clean", on_clean, "mean", mean_on_clean)
|
|||
|
|
print("I_on/I_cr/I_st abs mean", abs_Ion, abs_Icr, abs_Ist)
|
|||
|
|
print("only2", n_only2, sum_only2, "only3", n_only3, sum_only3)
|