Files
SystemSimulationApp/tests/manual/compare_local_probe.py
T
ljz 7611f13208 修复循环信号与事件采样并接入 LSTP 接触定位,补充八路验证及复用实验
相较上一版 Jacobian 确定性复用更新,本次补齐事件边界一致性、结果两侧采样及接触事件定位;保留已有物性复用和组件力学公式。

- 统一 UD00 信号求值与下一事件查询的绝对时间边界,修复循环边界浮点舍入导致的阶段错位、重复或漏报,并覆盖零时长、多阶段及长周期场景。
- 引入原生输出语义 v2:保留规则网格真实时间,补充内部时间事件和状态事件的左邻及事件后采样,按保存时间、状态和离散模式重放结果。
- 两条代码生成路径均发出 LSTP 接触描述,默认定位间隙过零及非负力模式的力截断;仅在接受事件时更新防重复记录,增加 contactEvents 诊断计数。
- 补充 MASS/LSTP 独立事件实验、八路全曲线与驱动阶段配对评估,以及 Amesim 不连续点输出对照和力差定位报告;MASS 新增释放机制仍保留为独立实验。
- 保存局部 probe、context 访问与回退、shadow replay、R288 real skip/typed replay 及阀门数值尾部诊断工具和报告;未证明净收益的实验不启用为生产默认优化。
- 更新原生运行说明和元件建模规范,补充信号边界、输出语义、接触事件和实验依赖回归测试。

验证:五组专项回归共 34 项全部通过;37 个待提交 Python 文件语法检查通过;git diff --cached --check 通过。
2026-09-17 23:50:13 +08:00

121 lines
14 KiB
Python
Raw 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.
"""Exact numerical gates and measured performance report for local probes."""
from pathlib import Path
import hashlib, json, statistics, subprocess
ROOT=Path(__file__).resolve().parents[2]
OUT=ROOT/'test/local-probe-20260917'
def read(path):return json.loads((OUT/path).read_text(encoding='utf-8'))
def table(headers,rows):return '\n| '+' | '.join(headers)+' |\n| '+' | '.join(['---']*len(headers))+' |\n'+'\n'.join('| '+' | '.join(map(str,row))+' |' for row in rows)+'\n'
def compare_matrices(left,right,n):
size=8*(1+n+n*n);checked=0
with left.open('rb') as a,right.open('rb') as b:
while True:
x,y=a.read(size),b.read(size)
if not x and not y:break
assert len(x)==len(y)==size,('matrix record size',checked)
assert x[:8*(1+n)]==y[:8*(1+n)],('Jacobian input t/y differs',checked)
assert x[8*(1+n):]==y[8*(1+n):],('Jacobian matrix bits differ',checked)
checked+=1
return dict(matrices=checked,entries=checked*n*n,differentEntries=0,inputTimesAndStatesIdentical=True)
def main():
plan=read('plan.json');reference=read('reference-audit/measurement.json')
cases=['group26-final-audit','small-final-audit','all-audit']
exact=['statesSha256','outputsSha256','eventsSha256','finalState','final','propertyWarnings',
'acceptedSteps','rejectedSteps','stateTransitions','solverStarts','nfev','njev','nlu']
checks=[]
for path in sorted(OUT.glob('*/measurement.json')):
obj=json.loads(path.read_text(encoding='utf-8'))
differences=[k for k in exact if obj[k]!=reference[k]]
for k in ('newtonIterations','newtonConvergenceFailures'):
if obj['diagnostic'][k]!=reference['diagnostic'][k]:differences.append(k)
checks.append(dict(run=path.parent.name,differences=differences))
assert not any(x['differences'] for x in checks),checks
matrices={name:compare_matrices(OUT/'reference-audit/jacobians.bin',OUT/name/'jacobians.bin',len(plan['stateKeys'])) for name in cases}
allrun=read('all-audit/measurement.json');diag=allrun['diagnostic'];groups=[]
for theory,actual in zip(plan['groups'],diag['groups']):
assert all(a+b==896 for a,b in zip(actual['executed'],actual['skipped']))
assert not any(actual['skipped'][i] for i in theory['affectedOperations'])
groups.append({**theory,**actual,'executedTotal':sum(actual['executed']),'skippedTotal':sum(actual['skipped'])})
metrics=['solveCpuSeconds','solveSeconds','processSeconds','jacobianSeconds']
measurements=[];medians={}
for name in ('reference','all'):
runs=[read(f'{name}-run-{i}/measurement.json') for i in range(5)]
for i,r in enumerate(runs):
measurements.append(dict(mode=name,run=i,**{k:r[k] for k in metrics[:-1]},jacobianSeconds=r['diagnostic']['jacobianSeconds']))
medians[name]={k:statistics.median(r['diagnostic'][k] if k=='jacobianSeconds' else r[k] for r in runs) for k in metrics}
reductions={k:1-medians['all'][k]/medians['reference'][k] for k in metrics}
source_meta=read('audit/build-metadata.json')
source_unchanged=all(hashlib.sha256((ROOT/'native'/key).read_text(encoding='utf-8').encode()).hexdigest()==value for key,value in source_meta['sourceHashes'].items())
assert source_unchanged
# The original evaluator, ordinary residual, temperature checker and linear
# solver are not patched. The added evaluator is reachable only from lp_eval.
original=(OUT/'original-model.c').read_text(encoding='utf-8')
for mode in ('audit','worker'):
emitted=(OUT/mode/'model.c').read_text(encoding='utf-8')
assert original in emitted,'original model source changed'
old=ROOT/'test/probe-state-residual-20260916/baseline-run-1'
prior={}
for name in ('states','outputs'):
if (old/f'{name}.bin').exists():
with (old/f'{name}.bin').open('rb') as f:prior[name]=hashlib.file_digest(f,'sha256').hexdigest()==reference[name+'Sha256']
assert all(prior.values())
result=dict(numericalChecks=checks,matrixComparisons=matrices,
shadow=dict(comparisons=diag['auditComparisons'],differentComparisons=diag['auditDifferences'],
comparedDerivativeAndOutputValues=diag['auditComparisons']*(len(plan['stateKeys'])+len(reference['final']))),
originalNativeSourcesUnchanged=source_unchanged,originalGeneratedModelVerbatim=True,priorUninstrumentedOutputsEqual=prior,
medians=medians,reductions=reductions,measurements=measurements,groups=groups,
workspaceBytes=diag['workspaceBytes'],copiedBytes=diag['contextCopiedBytes'],comparedBytes=diag['contextComparedBytes'])
(OUT/'comparison.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
report=[]
report.append('**局部 Jacobian probe 实验结果(2026-09-17)**\n')
report.append('本次增加独立实验构建工具,未接入前端或生产默认路径。生产 ordinary residual、物性算法、accepted-step check、线性求解器及原始生成模型源码保持不变;实验 worker 只在 Jacobian callback 中选择额外生成的局部求值函数。对象为 `tests/data/test-mql-8-corrected.json`,原提交 `1aac220`,CVODE 7.4.0,0–10 s,BDF,rtol=1e-8,沿用原 atol、步长和线性求解设置。\n')
report.append('**实施范围与保护**\n')
report.append('从现有 StateDependencies 和 EvaluationSchedule 保守计算各 group 的状态依赖闭包,未解析用户 C 或手写组件白名单。未知依赖按全部状态处理;当前实验拒绝包含循环代数块的模型。每组生成连续的不受扰动调度区间。扰动时间和组外状态必须与本次 Jacobian baseline 逐位一致,跨 Jacobian 基准失效。\n')
report.append('原型只裁剪 flow/stream/linear/alias 调度段。气体状态准备、后续节点能量、端口传播、机械方程、各管路诊断输出和有限值检查继续执行原代码。纯代数区间依据闭包复制基准的所有区间输出;包含上下文调用的区间,还要求入口的完整 live property states、count/capacity、observer/Jacobian 指针及全部 pipe cache 与 baseline 逐位一致,再恢复基准出口上下文和所有区间输出。任何不一致均执行原计算,无近似匹配、无放宽容差。\n')
report.append('保留 property context 中的 h/rho/黏度/等熵派生字段及种子顺序,避免只按显式 p,T 或 p,h 误判等价。已有 Jacobian 标量表在 baseline 后保持只读,跳过调用只减少其评估/复用统计,不改变表内数值。温度观察路径不走局部函数。\n')
report.append('初版第26组通过数值验证,但无条件保存所有快照导致约5.11 GB复制。最终版按启用组选择快照,并在纯代数操作之间共享上下文检查点:321个调度边界只需57个上下文检查点。只共享确定不含上下文调用的检查点;未放宽任何数值或上下文比较条件。\n')
report.append('**先验证数值等价**\n')
report.append(table(['验证阶段','完整矩阵数','逐位比较矩阵元素数','矩阵差异','每次Jacobian求值的dy/w对照'],[(name,matrices[name]['matrices'],matrices[name]['entries'],0,'25,088次,差异0') for name in cases]))
report.append('每份矩阵记录同时比较 t、132维输入 y 和132×132矩阵的原始双精度位,包含稀疏结构之外的零元素。另用独立 Jacobian memo workspace 在同一(t,y)执行原实现,比较每次 baseline/probe 的全部132个dy及1784个w;任意差异立即终止。该影子计算只在 audit worker 启用,不计入求解器nfev;性能 worker 完全关闭影子计算、矩阵落盘及内核入口计数。\n')
report.append(table(['项目','原实现','局部全部group','结果'],[
('states / outputs 原始二进制','完整文件','完整文件','逐字节一致'),('warning','全部告警字段','全部告警字段','一致'),
('event','事件时刻及132维事件后状态','同左','逐位一致'),
*[(k,reference[k],allrun[k],'一致') for k in ('acceptedSteps','rejectedSteps','stateTransitions','solverStarts','nfev','njev','nlu')],
('Newton iterations',reference['diagnostic']['newtonIterations'],diag['newtonIterations'],'一致'),
('Newton convergence failures',reference['diagnostic']['newtonConvergenceFailures'],diag['newtonConvergenceFailures'],'一致')]))
report.append('所有已保存运行的完整结果均对照通过;还核对了前一轮真正未插桩 worker 的 states/outputs,两者逐字节相同。图闭包及循环块拒绝的5项单元检查通过;原生 guard 测试验证了同p,T但派生rho改变、pipe cache改变、上下文长度/指针改变、t改变、组外状态改变和跨基准失效均不会误命中,并验证纯代数区间恢复不会覆盖当前物性上下文。未发现需要用容差掩盖的依赖裁剪差异。\n')
report.append('**实际执行范围**\n')
total=sum(g['executedTotal']+g['skippedTotal'] for g in groups);skipped=sum(g['skippedTotal'] for g in groups)
report.append(f'Jacobian次数仍为896,每个Jacobian仍调用1次baseline及27次probe,合计25,088次,其中probe为24,192次。probe 调度操作从 **{total:,}** 次降为 **{total-skipped:,}** 次,实际跳过 **{skipped:,}({skipped/total:.2%})**。与此前88.12%的结构无关操作比例相比,完整上下文保护、保留的诊断/后续操作限制了可安全跳过的范围。\n')
report.append(table(['group','扰动状态数','图上受影响操作数/484','实际执行操作/896次probe','跳过比例','上下文保护回退次数'],[(g['color'],len(g['stateIndices']),len(g['affectedOperations']),g['executedTotal'],f'{g["skippedTotal"]/(484*896):.2%}',g['contextMisses']) for g in groups]))
report.append('每组的被扰动状态、受影响操作编号、每一项操作的执行和跳过计数均保存在 `comparison.json`;操作编号、名称、输入输出和依赖状态在 `plan.json`。所有图上可能受影响操作的跳过次数均为0。下面统计24,192次probe实际进入内核的次数,不含baseline、影子计算或普通residual。\n')
kernel_rows=[]
for i,name in enumerate(diag['kernelNames']):
a=sum(row[i] for row in reference['diagnostic']['kernelCalls'][1:]);b=sum(row[i] for row in diag['kernelCalls'][1:])
kernel_rows.append((name,a,b,f'{1-b/a:.2%}' if a else '—'))
report.append(table(['实际计算','原实现','局部probe','减少'],kernel_rows))
report.append('gas、PH、density和pipe root的实际调用未变;已有精确缓存本来就已避免大量无关求根。此次节省来自流量、等熵、黏度及属性查找的实际执行。管路诊断仍全部执行,未将其删除或替换为未验证的近似值。\n')
report.append('**实际耗时**\n')
report.append('同一不含影子核验的可执行文件,通过 LOCAL_PROBE_MASK=0 和全组mask切换;排除一次预热,5对交替顺序串行测量,每次均完整输出states/outputs。Jacobian为整个callback的QPC墙钟时间;积分CPU为现有整进程CPU计时;完整native仿真时间从启动worker到输出完成并退出,包含DLL加载、初始化、积分、采样、输出重放/编码/落盘及退出。该数字不包含浏览器、API排队或已完成的编译。\n')
names={'jacobianSeconds':'Jacobian阶段墙钟','solveCpuSeconds':'积分CPU','solveSeconds':'积分墙钟','processSeconds':'完整native仿真墙钟'}
report.append(table(['中位数','原实现 / s','局部全部group / s','减少'],[(names[k],f'{medians["reference"][k]:.6f}',f'{medians["all"][k]:.6f}',f'{reductions[k]:.2%}') for k in ('jacobianSeconds','solveCpuSeconds','solveSeconds','processSeconds')]))
report.append(table(['序号/模式','Jacobian / s','积分CPU / s','积分墙钟 / s','完整native / s'],[(f'{r["run"]}/{r["mode"]}',*[f'{r[k]:.6f}' for k in ('jacobianSeconds','solveCpuSeconds','solveSeconds','processSeconds')]) for r in measurements]))
report.append('全部样本保留,包括原实现第2次较慢的样本,没有按有利结果筛选。当前测量显示收益,但绝对秒数受机器负载和频率影响;不能把前一日7.07s与今日结果直接相减归为优化收益。\n')
report.append(f'最终局部工作区占 **{diag["workspaceBytes"]:,} 字节**。整轮额外上下文复制 **{diag["contextCopiedBytes"]:,} 字节**、比较请求覆盖 **{diag["contextComparedBytes"]:,} 字节**;memcmp可能提前终止,后者不是硬件实际读取字节数。上述机制的开销已经包含在性能测量中。还包括switch分派、组外状态检查和恢复输出。\n')
cold=[]
for name in ('reference','all'):
path=OUT/f'cold-{name}-pipeline.json'
if path.exists():cold.append(json.loads(path.read_text()))
if cold:
report.append('另外从工程JSON开始执行实验的冷构建完整链(读取/方程生成/实验代码生成/GCC编译/DLL准备/worker运行/输出),实测如下。两种运行使用同一实验构建机制,冷编译时间会波动;这不是前端API链路测量,也不能用单次冷启动数据判断probe收益。\n')
report.append(table(['冷构建链','准备与构建 / s','完整native / s','完整实验流水线 / s'],[(x['mode'],f'{x["prepareSeconds"]:.6f}',f'{x["workerSeconds"]:.6f}',f'{x["pipelineSeconds"]:.6f}') for x in cold]))
report.append('**边界与后续方向**\n')
report.append('此版本可作为验证原型,暂不建议直接接入默认求解路径:仅支持当前无循环调度的生成模型,快照内存随模型规模增长,实验状态为独立worker内的静态运行期对象,不适合直接嵌入同进程并行仿真。较大group的属性上下文经常变化,保守整上下文比较会使本可独立的区间回退,收益小于最初理论无关计算预算。\n')
report.append('下一步若继续推进,应先研究保留完整读写语义的更细粒度context依赖,再扩展到诊断/其他阶段。不能直接删除上下文保护,也不能把88.12%的无关操作比例解释为可实现的同等加速。当前实验已证明可以在不改变数值轨迹的前提下减少一部分probe计算,但没有消除全部无关执行。\n')
(OUT/'report.md').write_text('\n'.join(report),encoding='utf-8')
print(json.dumps(dict(medians=medians,reductions=reductions,matrices=matrices,skippedOperations=skipped,operationReduction=skipped/total,numericalRuns=len(checks)),ensure_ascii=False,indent=2))
if __name__=='__main__':main()