sync: SHANGHAI-U9 2026-07-27 三个 Simulink→RTL skill 转系统级 + 新增 simulink-rtl-redev
- simulink_rtl_dev / rtl-ip-replace: 从工程级 .claude/skills 迁到用户级并去工程耦合 (模型名/Notes 文档依赖/工程实测基线移出; addpath 与脚本路径改用户级) - rtl-ip-replace/scripts/gen_cmp_tb.py: Vivado 路径不再写死, 改读 VIVADO_PATH / XILINX_VIVADO (兼容 install root / bin / bin\vivado.bat 三种写法), 回落时提示 去设 skills_env.md 的 VIVADO_PATH - simulink-rtl-redev: 新增。原生模型改动后的仿真模型同步规范, 含两个实测过的工具 check_sync.py (三条时序链新鲜度闸门) 与 measure_k.m (时域/频域/包络三估计器互证) - sim-rtl-flow/SKILL.md: 加入 sync_sim:* 步骤, 引用上述三个 skill
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
---
|
||||
name: simulink-rtl-redev
|
||||
description: >
|
||||
改动原生模型(xxx_h.slx)之后,如何保证仿真模型(xxx_hs.slx)同步、仿真结论仍然算数。
|
||||
典型触发场景:Vivado 综合时序不满足要改流水、硬件调试发现问题要改 RTL、定点位宽调整。
|
||||
核心:生成脚本不会把原生核的改动翻译成 m 代码,只改一侧 = RTL 已变而仿真还在验证旧行为。
|
||||
提供两个工具:同步新鲜度闸门 check_sync.py(三条时序链,退出码即结论)、
|
||||
延迟实测 measure_k.m(时域/频域/包络三个估计器互证,不一致返回 NaN 而非猜测值)。
|
||||
另含改动分类、验证三步、环内/环外加流水的差别、六条禁令。
|
||||
触发词:改了原生模型、同步仿真模型、xxx_h 和 xxx_hs 不一致、时序违例改流水、
|
||||
硬件调试改RTL、仿真模型过时、等价闸门、cycle-true 同步、check_sync、measure_k、
|
||||
测延迟、K 是多少、加流水延迟多少拍、原生模型 仿真模型 双轨、改完怎么验、
|
||||
makehdl 之前要做什么。
|
||||
规范全文见工程 Notes/Task16-3.md。适用于任何"原生模型+仿真模型"双轨结构的工程。
|
||||
---
|
||||
|
||||
# simulink-rtl-redev — 原生模型改动后的仿真模型同步
|
||||
|
||||
## 这个 skill 解决什么
|
||||
|
||||
两个模型描述同一段算法:
|
||||
|
||||
| 记号 | 名称 | 内容 | 用途 |
|
||||
|---|---|---|---|
|
||||
| `xxx_h.slx` | 原生模型(-e) | 目标模块全部用可综合原生块搭建 | **生成 RTL** |
|
||||
| `xxx_hs.slx` | 仿真模型(-x/-q) | 由原生模型派生,跑不动的**原生核**换成等价的 **m 实现** | **跑场景仿真** |
|
||||
|
||||
**核心原则:改了原生模型,必须同步改仿真模型,并重新验证两者行为一致。**
|
||||
|
||||
原因:从原生模型派生仿真模型的脚本只做"摘掉原生核、换上 m 实现、接线",
|
||||
**它不会把原生核的改动翻译成 m 代码**。只改原生模型再重跑生成脚本,
|
||||
仿真模型不会跟着变——它只是把同一份未改动的 m 实现重新包一遍。
|
||||
|
||||
后果:**RTL 已经变了,而仿真还在验证旧行为**。板上出问题时仿真给不出线索,
|
||||
因为它测的根本不是同一个设计。
|
||||
|
||||
## 什么时候需要手工同步
|
||||
|
||||
只看改动落在哪里:
|
||||
|
||||
| 改动位置 | 是否需要手工同步 m 实现 |
|
||||
|---|---|
|
||||
| **原生核之内** | **需要**。这是两套实现的重叠区 |
|
||||
| 原生核之外(其余链路) | 不需要。生成脚本原样继承,重生成即可 |
|
||||
|
||||
## Agent 路径:先跑闸门
|
||||
|
||||
任何时候怀疑"仿真结论还算不算数",第一件事是跑同步新鲜度闸门。
|
||||
|
||||
### 一次性:写 manifest
|
||||
|
||||
在工程里建一份 manifest(建议放 `Notes/sync_manifest_<model>.json`),
|
||||
声明这个模型的源与派生产物。**`root` 相对 manifest 所在目录**,其余路径相对 `root`:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "<model>",
|
||||
"root": "..",
|
||||
|
||||
"native_model": "matlab/<proj>/xxx_h.slx",
|
||||
"build_scripts": ["matlab/<proj>/build_core.m"],
|
||||
"gen_scripts": ["matlab/<proj>/make_sim.m"],
|
||||
"mcode_impl": ["matlab/<proj>/core_mcode.m"],
|
||||
"runtime_deps": ["matlab/<proj>/pscore.m", "matlab/<proj>/coef.mat"],
|
||||
|
||||
"sim_model": "matlab/<proj>/xxx_hs.slx",
|
||||
"rtl_source_model": "matlab/<proj>/xxx_h_<Mod>_E.slx",
|
||||
|
||||
"gate_result": "Notes/gate_<model>.txt",
|
||||
"gate_tools": ["matlab/<proj>/gate.m", "matlab/<proj>/cmp_core.m"]
|
||||
}
|
||||
```
|
||||
|
||||
`runtime_deps` 必填**运行时才从路径解析的 .m** 与运行时读取的数据文件
|
||||
(`.mat`、系数表、数据字典)。改它们时模型本身一个字节都不变,漏列等于没查。
|
||||
`rtl_source_model` 与 `gate_tools` 可省(省了就不查对应项)。
|
||||
|
||||
### 每次改完模型:跑闸门
|
||||
|
||||
```
|
||||
cd <工程根>
|
||||
python "%USERPROFILE%\.claude\skills\simulink-rtl-redev\check_sync.py" Notes\sync_manifest_<model>.json
|
||||
```
|
||||
|
||||
Linux/macOS 把路径换成 `~/.claude/skills/simulink-rtl-redev/check_sync.py`。
|
||||
|
||||
**退出码即结论**:`0` 通过,`1` 有过时产物,`2` manifest 有问题(路径不存在等)。
|
||||
可直接串进脚本做前置门禁。
|
||||
|
||||
选项:`--quiet` 不打印文件清单,`--json` 输出机器可读结果。
|
||||
|
||||
它查三条时序链:
|
||||
|
||||
```
|
||||
链 1(派生新鲜):max(源文件) < min(派生产物)
|
||||
链 2(闸门新鲜):max(派生产物) < 闸门结果
|
||||
链 3(工具新鲜):max(闸门工具) < 闸门结果
|
||||
```
|
||||
|
||||
**链 1 是最容易漏的那条。** 只查"闸门晚于全部源文件"会放行下面这种情况:
|
||||
闸门确实在改完源之后跑的,但仿真模型停留在改动之前。本工具在真实工程上正是这样
|
||||
抓出了一个已存在的漂移——链 2、链 3 都 PASS,只有链 1 FAIL。
|
||||
|
||||
## 同步流程
|
||||
|
||||
```
|
||||
[1] 改原生实现(改构建脚本,不直接手改 .slx)
|
||||
|
|
||||
[2] 把同样的改动落到 m 实现 <-- 漏掉这步是本 skill 存在的理由
|
||||
|
|
||||
[3] 重新生成仿真模型
|
||||
|
|
||||
[4] 若改动带来延迟变化:先实测新增拍数 K
|
||||
|
|
||||
[5] 跑等价闸门 --> 原生核 与 m 实现 是否一致
|
||||
|
|
||||
[6] 跑短程冒烟 --> 链路是否还通
|
||||
|
|
||||
[7] 跑全场景全长 --> 对照基线,指标是否达标
|
||||
|
|
||||
[8] 从原生模型生成 RTL
|
||||
```
|
||||
|
||||
任一步不过,回到 `[1]` 重走,不要从中间插入——仿真模型一旦重生成,后面的结果全部作废。
|
||||
|
||||
**`[4]` 必须在 `[5]` 之前**:延迟变了,闸门比对要带偏移,K 没测出来这一步做不了。
|
||||
|
||||
跑完 `[3]` 之后、进 `[5]` 之前,先用 `check_sync.py` 确认新鲜度,能省掉一次白跑的闸门。
|
||||
|
||||
## 三类改动的差别
|
||||
|
||||
| 类别 | 例 | m 实现要改吗 | 延迟 | 场景判据 |
|
||||
|---|---|---|---|---|
|
||||
| **不改延迟** | 资源共享、块重构、常量折叠 | 通常不用 | 不变 | 与基线**逐位相同** |
|
||||
| **改延迟** | 插流水寄存器 | 补等量延迟 | +K | 吸收 K 后与基线**逐位相同** |
|
||||
| **改定点** | 位宽、二进制点、饱和/舍入 | **必须改** | 不变 | 行为已变,**重新定性并建立新基线** |
|
||||
|
||||
- **延迟必须实测,不能按设计意图推算。** 用 `measure_k.m`(下节),至少两种互不依赖的方法测出一致值。
|
||||
- **在反馈环内插流水,代价远大于环外。** 环外只是一个可被注入侧参数吸收的常数延迟;
|
||||
环内会改变环路群延迟,必须重新定标环路参数(步长、移位、门限),并先在单元台验证
|
||||
阶跃响应再上全链。**能搬到环外就搬到环外。**
|
||||
|
||||
## 实测新增延迟 K:`measure_k.m`
|
||||
|
||||
流程 `[4]`。B 类(加流水)改动必须先测 K 才能进 `[5]`——闸门的判据本身要用 K 作偏移。
|
||||
|
||||
```matlab
|
||||
addpath(fullfile(getenv('USERPROFILE'),'.claude','skills','simulink-rtl-redev'));
|
||||
[K, R] = measure_k(ref, dut); % ref/dut 为实数或复数 IQ 向量,同采样率
|
||||
```
|
||||
|
||||
`ref` 是改动前(或参考抽头)的信号,`dut` 是改动后(滞后 K 拍)的信号。
|
||||
返回 `K` = 认定的整数延迟,**方法不一致时返回 `NaN`**,不给猜测值。
|
||||
`R` 里有各方法的分项结果。可选参数:`'MaxLag'`(4096)、`'OnsetTol'`(1e-3)、
|
||||
`'FracTol'`(0.05)、`'Verbose'`(true)。**只用基础 MATLAB**,不依赖 Signal Processing Toolbox。
|
||||
|
||||
三个估计器,前两个域完全不同、始终可用,第三个仅在信号有静默前导时适用:
|
||||
|
||||
| 方法 | 域 | 说明 |
|
||||
|---|---|---|
|
||||
| 1 互相关 argmax | 时域 | 整数 |
|
||||
| 2 互谱相位斜率 | **频域** | 分数;纯延迟使 `angle(Y·conj(X))` 对频率成直线,斜率即 −K |
|
||||
| 3 起始点差 | 幅度包络 | 整数;阈值取各自**峰值**的比例,故 DUT 的噪声底不会误触发;连续激励信号判 `n/a` 而非失败 |
|
||||
|
||||
判据:方法 1 与 2 必须一致;方法 3 适用时也必须一致;插值峰距整数不超过 `FracTol`。
|
||||
|
||||
**为什么非要两种。** 实测:两路**完全无关**的信号,相关器照样给出 `K = 241`
|
||||
(|rho| 仅 0.0198 的假峰)——单靠它会把一个纯噪声峰当成延迟。方法 2 给出 −26.350,
|
||||
两者互不相认,工具才判 FAIL。这就是"至少两种互不依赖的方法"的全部理由。
|
||||
|
||||
实测过的六种情形:
|
||||
|
||||
| 情形 | 结果 |
|
||||
|---|---|
|
||||
| 干净整数延迟 40,复数 IQ | PASS,3 法一致 |
|
||||
| 增益 0.37× + 全程噪声底 | PASS,2 法一致(方法 3 自动判 n/a) |
|
||||
| 连续激励无静默段,延迟 17 | PASS,2 法一致 |
|
||||
| **分数延迟 12.5 拍** | **FAIL**:方法 1 给 13、方法 2 给 12.500,且插值峰偏离整数 0.5 |
|
||||
| **两路无关信号** | **FAIL**:241 vs −26.350 |
|
||||
| 真延迟 40 + 3 阶非线性失真 | PASS,2 法一致(\|rho\| 0.989) |
|
||||
|
||||
分数延迟那一条尤其要留意:它说明改动**没有引入干净的整数流水延迟**(碰到了分数内插/重采样
|
||||
路径)。这时不能四舍五入接着走,要回去查改的是不是环内。
|
||||
|
||||
## 等价闸门的语义(极易搞错)
|
||||
|
||||
闸门比对的是**原生核与 m 实现这两套实现**,不是"新行为与旧行为"。因此:
|
||||
|
||||
- 闸门对**所有类别**的改动都必须跑,判据都是"一致";
|
||||
- 即使行为相对上一版变了,只要两侧按同样意图改完,**两套实现对新行为的描述仍必须一致**;
|
||||
- 改完两侧仍不一致,说明两边理解有分歧——这正是闸门要抓的。
|
||||
|
||||
**不要以"行为变了"为由跳过闸门。** 跳过它就等于放弃了对双实现是否同步的唯一检查。
|
||||
|
||||
闸门的局限:它只证明"在所用激励下一致",不证明全状态空间等价。
|
||||
状态机边界、饱和、复位时序这类角落必须补定向用例,不能指望随机激励覆盖到。
|
||||
|
||||
## 六条禁令
|
||||
|
||||
| # | 规矩 | 理由 |
|
||||
|---|---|---|
|
||||
| 1 | 仿真模型是**产物**,不能手改;要改就改生成脚本再重生成 | 手改必被下次重生成冲掉,git 上无法溯源 |
|
||||
| 2 | 只改一侧的改动**视为未完成**,不得进入验证 | 两套实现已分叉 |
|
||||
| 3 | 没跑过闸门,**不得跑场景仿真,也不得生成 RTL** | 前者测的可能是旧设计,后者错误直接进硅 |
|
||||
| 4 | 旧结果**永不覆盖**,每次运行带唯一标签、落唯一目录 | 失败记录是"改动前后对比"的另一半;陈旧编译缓存会静默复现旧结果 |
|
||||
| 5 | 判据不得为了通过而修改 | 一条带机理分析的 FAIL,价值高于一个来路不明的 PASS |
|
||||
| 6 | 时序违例优先改模型,**禁止**用多周期豁免放宽慢角富余路径 | 布线器会吃满被放松的预算,表现为"报告通过、板上异常" |
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **时间戳只是权宜之计。** `copy`/`touch`/版本签出都会扰动 mtime。
|
||||
`check_sync.py` 因此只能作自查,不是证明。更可靠的做法是让生成脚本与闸门自动记录
|
||||
所用源码的校验和,运行前比对不符即拒绝开跑——那属于工具建设,本 skill 不含。
|
||||
- **"闸门比全部源文件新"不等于没问题。** 这正是链 1 存在的理由,见上文。
|
||||
- **`runtime_deps` 漏列 = 没查。** 运行时解析的 .m 和 `.mat`/系数表改动时,
|
||||
模型校验和纹丝不动。这类依赖必须显式声明,工具无法自动发现
|
||||
(`load('x.mat')` 这种字符串依赖追不到)。
|
||||
- **manifest 里每个声明的路径都必须存在**,否则 exit 2。这是故意的:
|
||||
路径写错而静默跳过,比报错危险得多。
|
||||
- **exit 2 与 exit 1 含义不同**:2 是 manifest 坏了(你的配置问题),
|
||||
1 是真的有过时产物(你的工程问题)。串脚本时别把两者混为一谈。
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| 症状 | 原因 / 处置 |
|
||||
|---|---|
|
||||
| `ERROR: manifest refers to something that is missing -> <key>: <path>` | 该路径不存在。exit 2。检查 `root` 与相对路径;`root` 是相对 manifest 所在目录的 |
|
||||
| 链 1 FAIL | 派生产物早于源。**先**把原生核改动落到 m 实现,**再**重生成,**再**重跑闸门。只重生成不同步 m 实现无效 |
|
||||
| 链 2 FAIL | 闸门跑在当前派生产物之前 —— 重跑闸门 |
|
||||
| 链 3 FAIL | 闸门工具在记录的通过之后改过 —— 重跑闸门 |
|
||||
| 三条链全 PASS 但板上仍与仿真不符 | 新鲜度没问题,问题在别处:查闸门覆盖(定向用例是否覆盖到出问题的状态),或查 `runtime_deps` 是否漏列 |
|
||||
| `ERROR: manifest declares no sources / no derived products` | manifest 缺 `native_model` 一类的键,或值是空列表 |
|
||||
|
||||
## 规范全文
|
||||
|
||||
本 skill 是 `Notes/Task16-3.md` 的可执行化。规范原文(含制定过程与十轮评审纠正的
|
||||
逻辑错误)见该文档;本 skill 只承载执行路径与工具。
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freshness gate for the native-model / sim-model dual-track flow (Task16-3 section 8).
|
||||
|
||||
python check_sync.py <manifest.json> [--json] [--quiet]
|
||||
|
||||
Two models describe the same algorithm:
|
||||
|
||||
xxx_h.slx native model -- the ONLY source RTL is generated from
|
||||
xxx_hs.slx sim model -- derived from it, native core swapped for a
|
||||
hand-written m implementation, runs the scenarios
|
||||
|
||||
The m implementation is NOT derived from the native core: regenerating the sim
|
||||
model just re-wraps the same unchanged m file. So a change to the native core
|
||||
that is not mirrored into the m implementation leaves the RTL changed while the
|
||||
simulation still validates the OLD behaviour -- silently.
|
||||
|
||||
This tool checks the three ordering chains that catch that:
|
||||
|
||||
chain 1 (derived fresh) : max(sources) < min(derived products)
|
||||
chain 2 (gate fresh) : max(derived) < gate result
|
||||
chain 3 (tools fresh) : max(gate tools) < gate result
|
||||
|
||||
Chain 1 is the one people forget. "gate is newer than every source" alone
|
||||
passes even when the sim model was built before the source changed.
|
||||
|
||||
Exit code 0 = all chains hold, the sim evidence may be trusted.
|
||||
Exit code 1 = a chain is broken (stale artefact) -- regenerate and re-run the gate.
|
||||
Exit code 2 = manifest problem (missing file, bad JSON).
|
||||
|
||||
Timestamps are a stopgap: copy/touch/checkout perturb them. The stronger form is
|
||||
to have the generator and gate record checksums of the sources they used. This
|
||||
tool is the cheap version that needs no changes to the existing scripts.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
# manifest key -> (role, which side of chain 1)
|
||||
SOURCE_KEYS = [
|
||||
('native_model', 'native model'),
|
||||
('build_scripts', 'build script'),
|
||||
('gen_scripts', 'generator script'),
|
||||
('mcode_impl', 'm implementation'),
|
||||
('runtime_deps', 'runtime dep (.m / .mat / config)'),
|
||||
]
|
||||
DERIVED_KEYS = [
|
||||
('sim_model', 'sim model'),
|
||||
('rtl_source_model', 'RTL source model'),
|
||||
]
|
||||
|
||||
|
||||
def _as_list(v):
|
||||
if v is None:
|
||||
return []
|
||||
return v if isinstance(v, list) else [v]
|
||||
|
||||
|
||||
def _resolve(root, p):
|
||||
return p if os.path.isabs(p) else os.path.normpath(os.path.join(root, p))
|
||||
|
||||
|
||||
def collect(root, manifest, keys):
|
||||
"""-> list of (abspath, role, mtime) ; missing files raise."""
|
||||
out = []
|
||||
for key, role in keys:
|
||||
for p in _as_list(manifest.get(key)):
|
||||
ap = _resolve(root, p)
|
||||
if not os.path.exists(ap):
|
||||
raise FileNotFoundError(f'{key}: {ap}')
|
||||
out.append((ap, role, os.path.getmtime(ap)))
|
||||
return out
|
||||
|
||||
|
||||
def ts(t):
|
||||
return datetime.fromtimestamp(t).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
|
||||
def rel(root, p):
|
||||
try:
|
||||
return os.path.relpath(p, root)
|
||||
except ValueError:
|
||||
return p
|
||||
|
||||
|
||||
def main(argv):
|
||||
args = [a for a in argv if not a.startswith('--')]
|
||||
flags = {a for a in argv if a.startswith('--')}
|
||||
if not args:
|
||||
sys.exit(__doc__)
|
||||
|
||||
mf_path = os.path.abspath(args[0])
|
||||
try:
|
||||
manifest = json.load(open(mf_path, encoding='utf-8-sig'))
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f'ERROR: cannot read manifest {mf_path}: {e}')
|
||||
return 2
|
||||
|
||||
# paths in the manifest are relative to "root", default = manifest's directory
|
||||
root = manifest.get('root') or os.path.dirname(mf_path)
|
||||
root = os.path.abspath(_resolve(os.path.dirname(mf_path), root))
|
||||
|
||||
try:
|
||||
sources = collect(root, manifest, SOURCE_KEYS)
|
||||
derived = collect(root, manifest, DERIVED_KEYS)
|
||||
tools = collect(root, manifest, [('gate_tools', 'gate tool')])
|
||||
gate_p = manifest.get('gate_result')
|
||||
if not gate_p:
|
||||
raise KeyError('gate_result')
|
||||
gate_ap = _resolve(root, gate_p)
|
||||
if not os.path.exists(gate_ap):
|
||||
raise FileNotFoundError(f'gate_result: {gate_ap}')
|
||||
gate_t = os.path.getmtime(gate_ap)
|
||||
except (FileNotFoundError, KeyError) as e:
|
||||
print(f'ERROR: manifest refers to something that is missing -> {e}')
|
||||
print(' every declared path must exist; fix the manifest or generate the file.')
|
||||
return 2
|
||||
|
||||
if not sources:
|
||||
print('ERROR: manifest declares no sources'); return 2
|
||||
if not derived:
|
||||
print('ERROR: manifest declares no derived products'); return 2
|
||||
|
||||
newest_src = max(sources, key=lambda r: r[2])
|
||||
oldest_der = min(derived, key=lambda r: r[2])
|
||||
newest_der = max(derived, key=lambda r: r[2])
|
||||
newest_tool = max(tools, key=lambda r: r[2]) if tools else None
|
||||
|
||||
chains = [
|
||||
('1 derived fresh', newest_src[2] < oldest_der[2],
|
||||
f'newest source {ts(newest_src[2])} {rel(root, newest_src[0])}',
|
||||
f'oldest derived {ts(oldest_der[2])} {rel(root, oldest_der[0])}'),
|
||||
('2 gate fresh', newest_der[2] < gate_t,
|
||||
f'newest derived {ts(newest_der[2])} {rel(root, newest_der[0])}',
|
||||
f'gate result {ts(gate_t)} {rel(root, gate_ap)}'),
|
||||
]
|
||||
if newest_tool:
|
||||
chains.append(('3 tools fresh', newest_tool[2] < gate_t,
|
||||
f'newest tool {ts(newest_tool[2])} {rel(root, newest_tool[0])}',
|
||||
f'gate result {ts(gate_t)} {rel(root, gate_ap)}'))
|
||||
|
||||
ok = all(c[1] for c in chains)
|
||||
|
||||
if '--json' in flags:
|
||||
print(json.dumps({
|
||||
'model': manifest.get('model'),
|
||||
'ok': ok,
|
||||
'chains': [{'name': n, 'ok': o} for n, o, _, _ in chains],
|
||||
'gate_result_mtime': ts(gate_t),
|
||||
}, ensure_ascii=False, indent=2))
|
||||
return 0 if ok else 1
|
||||
|
||||
print(f'check_sync: {manifest.get("model") or "(unnamed)"} root={root}')
|
||||
if '--quiet' not in flags:
|
||||
print('\n all declared files, oldest first:')
|
||||
for ap, role, t in sorted(sources + derived + tools + [(gate_ap, 'gate result', gate_t)],
|
||||
key=lambda r: r[2]):
|
||||
print(f' {ts(t)} {role:<32} {rel(root, ap)}')
|
||||
print()
|
||||
for name, good, a, b in chains:
|
||||
print(f' [{"PASS" if good else "FAIL"}] chain {name}')
|
||||
print(f' {a}')
|
||||
print(f' {b}')
|
||||
if not good:
|
||||
print(' -> the second is OLDER than the first, i.e. it is STALE')
|
||||
print()
|
||||
if ok:
|
||||
print('VERDICT: PASS -- ordering holds; sim evidence may be trusted.')
|
||||
else:
|
||||
print('VERDICT: FAIL -- a stale artefact is in play.')
|
||||
broken = {c[0][0] for c in chains if not c[1]} # '1' / '2' / '3'
|
||||
if '1' in broken:
|
||||
print(' chain 1: the derived model predates its source.')
|
||||
print(' FIRST mirror any native-core change into the m implementation,')
|
||||
print(' THEN regenerate the derived model(s), THEN re-run the gate.')
|
||||
print(' Regenerating alone does NOT sync the m implementation.')
|
||||
if '2' in broken:
|
||||
print(' chain 2: the gate ran before the current derived model existed -- re-run the gate.')
|
||||
if '3' in broken:
|
||||
print(' chain 3: the gate tooling changed after the recorded pass -- re-run the gate.')
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
@@ -0,0 +1,199 @@
|
||||
function [K, R] = measure_k(ref, dut, varargin)
|
||||
%MEASURE_K Measure the integer delay K that <dut> lags <ref>, by TWO INDEPENDENT
|
||||
% methods that must agree (Task16-3 section 5: "延迟必须实测,不能按设计意图推算.
|
||||
% 用独立微台、至少两种互不依赖的方法测出一致值").
|
||||
%
|
||||
% K = measure_k(ref, dut)
|
||||
% [K, R] = measure_k(ref, dut, 'Name', value, ...)
|
||||
%
|
||||
% ref, dut : real or COMPLEX vectors, same sample rate. dut is the signal that
|
||||
% comes out K samples later than ref.
|
||||
%
|
||||
% Options
|
||||
% 'MaxLag' (default 4096) search window, in samples
|
||||
% 'OnsetTol' (default 1e-3) onset threshold, RELATIVE to each signal's own
|
||||
% RMS -- so the two signals may differ in gain
|
||||
% 'FracTol' (default 0.05) how far the interpolated correlation peak may sit
|
||||
% from an integer before K is declared non-integer
|
||||
% 'Verbose' (default true)
|
||||
%
|
||||
% Returns
|
||||
% K : the agreed integer delay, or NaN if the methods disagree
|
||||
% R : struct with per-method results and the verdict --
|
||||
% .k_corr argmax of the normalised cross-correlation
|
||||
% .k_onset difference of first-significant-sample indices
|
||||
% .k_frac parabolic-interpolated peak (fractional)
|
||||
% .frac_resid |k_frac - round(k_frac)| -- large => not a pure integer shift
|
||||
% .rho |correlation| at the peak, 0..1
|
||||
% .agree k_corr == k_onset
|
||||
% .pass agree && frac_resid <= FracTol
|
||||
%
|
||||
% WHY TWO METHODS: a correlation argmax alone will happily return a number for
|
||||
% two unrelated signals. Onset detection is a structurally different estimator
|
||||
% (amplitude threshold, not inner product), so agreement between them is real
|
||||
% evidence. The fractional peak is the third check: it catches the case where
|
||||
% the change did NOT introduce a clean integer pipeline delay (e.g. it altered a
|
||||
% fractional interpolator), which an integer-only estimator would silently round.
|
||||
%
|
||||
% Base MATLAB only -- no Signal Processing Toolbox (no xcorr/finddelay).
|
||||
|
||||
p = inputParser;
|
||||
p.addParameter('MaxLag', 4096, @(x) isnumeric(x) && isscalar(x) && x > 0);
|
||||
p.addParameter('OnsetTol', 1e-3, @(x) isnumeric(x) && isscalar(x) && x > 0);
|
||||
p.addParameter('FracTol', 0.05, @(x) isnumeric(x) && isscalar(x) && x > 0);
|
||||
p.addParameter('Verbose', true, @(x) islogical(x) || isnumeric(x));
|
||||
p.parse(varargin{:});
|
||||
o = p.Results;
|
||||
|
||||
ref = ref(:); dut = dut(:);
|
||||
n = min(numel(ref), numel(dut));
|
||||
if n < 16
|
||||
error('measure_k: need at least 16 samples, got %d', n);
|
||||
end
|
||||
ref = ref(1:n); dut = dut(1:n);
|
||||
|
||||
maxlag = min(o.MaxLag, n - 1);
|
||||
|
||||
% ---- method 1: normalised cross-correlation, argmax over 0..maxlag -----------
|
||||
% FFT-based so it stays base MATLAB and is fast for long vectors.
|
||||
nfft = 2^nextpow2(2*n);
|
||||
Rxy = ifft(fft(dut, nfft) .* conj(fft(ref, nfft))); % dut delayed by k => peak at k
|
||||
c = abs(Rxy(1:maxlag+1));
|
||||
den = norm(ref) * norm(dut);
|
||||
if den == 0
|
||||
error('measure_k: one of the signals is all zeros');
|
||||
end
|
||||
c = c / den;
|
||||
[rho, idx] = max(c);
|
||||
k_corr = idx - 1;
|
||||
|
||||
% ---- method 3 (on the same curve): parabolic interpolation of the peak -------
|
||||
% Done here because it needs c; it is reported separately and is NOT one of the
|
||||
% "two independent methods" -- it only tests integer-ness.
|
||||
if idx > 1 && idx < numel(c)
|
||||
y0 = c(idx-1); y1 = c(idx); y2 = c(idx+1);
|
||||
d = (y0 - y2) / (2*(y0 - 2*y1 + y2));
|
||||
if ~isfinite(d), d = 0; end
|
||||
k_frac = k_corr + d;
|
||||
else
|
||||
k_frac = k_corr;
|
||||
end
|
||||
frac_resid = abs(k_frac - round(k_frac));
|
||||
|
||||
% ---- method 2: cross-spectral phase slope (FREQUENCY domain, always available)
|
||||
% A pure delay of K samples makes angle(Y.*conj(X)) a straight line of slope -K
|
||||
% against normalised frequency. Fitting that slope uses no argmax at all, so it
|
||||
% is a genuinely different estimator from method 1 -- and it is fractional, so it
|
||||
% doubles as an integer-ness check that cannot be fooled by rounding.
|
||||
k_phase = local_phase_slope(ref, dut, nfft);
|
||||
|
||||
% ---- method 3: onset of the first significant sample (only if one exists) ----
|
||||
% Threshold is a fraction of each signal's own PEAK envelope, not its RMS: a
|
||||
% noise/quantisation floor in the DUT during the reference's silent lead-in must
|
||||
% not trigger it. Returns NaN for continuously excited signals -- that is not a
|
||||
% failure, it just means this method does not apply.
|
||||
i_ref = local_onset(ref, o.OnsetTol);
|
||||
i_dut = local_onset(dut, o.OnsetTol);
|
||||
if isnan(i_ref) || isnan(i_dut)
|
||||
k_onset = NaN;
|
||||
else
|
||||
k_onset = i_dut - i_ref;
|
||||
end
|
||||
|
||||
% ---- adjudication -----------------------------------------------------------
|
||||
% Require methods 1 and 2 (always available, different domains) to agree, and
|
||||
% method 3 to agree as well WHEN it applies.
|
||||
agree_phase = isfinite(k_phase) && abs(k_phase - k_corr) <= o.FracTol;
|
||||
agree_onset = isnan(k_onset) || (k_onset == k_corr); % NaN = not applicable
|
||||
n_methods = 2 + double(~isnan(k_onset));
|
||||
agree = agree_phase && agree_onset;
|
||||
pass = agree && (frac_resid <= o.FracTol);
|
||||
if pass, K = k_corr; else, K = NaN; end
|
||||
|
||||
R = struct('k_corr', k_corr, 'k_phase', k_phase, 'k_onset', k_onset, ...
|
||||
'k_frac', k_frac, 'frac_resid', frac_resid, 'rho', rho, ...
|
||||
'agree_phase', agree_phase, 'agree_onset', agree_onset, ...
|
||||
'n_methods', n_methods, 'agree', agree, 'pass', pass, ...
|
||||
'n', n, 'maxlag', maxlag);
|
||||
|
||||
if o.Verbose
|
||||
fprintf('measure_k: n=%d maxlag=%d\n', n, maxlag);
|
||||
fprintf(' method 1 xcorr argmax (time domain) K = %d |rho| = %.6f\n', k_corr, rho);
|
||||
if isfinite(k_phase)
|
||||
fprintf(' method 2 phase slope (freq domain) K = %.3f\n', k_phase);
|
||||
else
|
||||
fprintf(' method 2 phase slope (freq domain) K = n/a (not enough usable bins)\n');
|
||||
end
|
||||
if isnan(k_onset)
|
||||
fprintf(' method 3 onset diff (amplitude) K = n/a (no clear onset -- continuous signal)\n');
|
||||
else
|
||||
fprintf(' method 3 onset diff (amplitude) K = %d\n', k_onset);
|
||||
end
|
||||
fprintf(' check interpolated peak %.3f (residual %.4f, tol %.4f)\n', ...
|
||||
k_frac, frac_resid, o.FracTol);
|
||||
if pass
|
||||
fprintf(' VERDICT: PASS -- %d independent methods agree, K = %d\n', n_methods, K);
|
||||
else
|
||||
fprintf(' VERDICT: FAIL -- K NOT established\n');
|
||||
if ~agree_phase
|
||||
fprintf(' methods 1 and 2 disagree (%d vs %.3f). Different domains giving\n', ...
|
||||
k_corr, k_phase);
|
||||
fprintf(' different answers means these are not a pure delayed copy of each\n');
|
||||
fprintf(' other -- check you compared the right taps.\n');
|
||||
end
|
||||
if ~agree_onset
|
||||
fprintf(' method 3 disagrees (%d vs %d). A noise/quantisation floor in the\n', ...
|
||||
k_corr, k_onset);
|
||||
fprintf(' DUT during the reference lead-in can do this: raise OnsetTol.\n');
|
||||
end
|
||||
if frac_resid > o.FracTol
|
||||
fprintf(' peak sits %.3f off an integer: the change did not introduce a\n', frac_resid);
|
||||
fprintf(' clean integer pipeline delay (fractional/resampling path?).\n');
|
||||
end
|
||||
fprintf(' Do NOT fall back to the design-intent value (Task16-3 section 5).\n');
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function kp = local_phase_slope(ref, dut, nfft)
|
||||
%LOCAL_PHASE_SLOPE Delay from the slope of the cross-spectrum phase.
|
||||
% Only bins where the reference has real energy are used, and the phase is
|
||||
% unwrapped over those bins before a weighted least-squares line fit.
|
||||
X = fft(ref, nfft); Y = fft(dut, nfft);
|
||||
half = floor(nfft/2);
|
||||
X = X(2:half); Y = Y(2:half); % drop DC and Nyquist
|
||||
w = abs(X);
|
||||
if isempty(w) || max(w) == 0, kp = NaN; return, end
|
||||
keep = w > 0.10 * max(w); % usable bins only
|
||||
if nnz(keep) < 16, kp = NaN; return, end
|
||||
k = (1:numel(X)).';
|
||||
ph = unwrap(angle(Y(keep) .* conj(X(keep))));
|
||||
kk = k(keep);
|
||||
ww = w(keep);
|
||||
% weighted least squares: ph ~ a + b*(2*pi*kk/nfft), delay = -b
|
||||
x = 2*pi*kk/nfft;
|
||||
sw = sum(ww);
|
||||
mx = sum(ww.*x)/sw; my = sum(ww.*ph)/sw;
|
||||
b = sum(ww.*(x-mx).*(ph-my)) / sum(ww.*(x-mx).^2);
|
||||
kp = -b;
|
||||
end
|
||||
|
||||
|
||||
function i0 = local_onset(x, tol)
|
||||
%LOCAL_ONSET First index where the SMOOTHED envelope exceeds tol * its own PEAK.
|
||||
% Threshold is relative to the peak, not the RMS, so a noise or quantisation
|
||||
% floor well below the signal does not trigger it. Returns NaN when the signal
|
||||
% is already at level from the first sample (continuously excited) -- that means
|
||||
% "this method does not apply", not "failure".
|
||||
a = abs(x);
|
||||
w = 8; % short moving average
|
||||
e = filter(ones(w,1)/w, 1, a);
|
||||
pk = max(e);
|
||||
if pk <= 0, i0 = NaN; return, end
|
||||
thr = tol * pk;
|
||||
i0 = find(e > thr, 1, 'first');
|
||||
if isempty(i0), i0 = NaN; return, end
|
||||
% already hot at the very start => no usable onset
|
||||
if i0 <= w, i0 = NaN; end
|
||||
end
|
||||
Reference in New Issue
Block a user