Files
SystemSimulationApp/tests/manual/profile_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

219 lines
14 KiB
Python

"""Sampled, exclusive timing of a COPY of the local-probe performance worker.
Original numerical functions remain in the executable verbatim. Only sampled
Jacobian callbacks use timing clones; ordinary residuals never reach a clone.
No production source or existing experiment implementation is edited.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import argparse, hashlib, json, os, re, shutil, statistics, subprocess, time
import local_probe_experiment as experiment
ROOT=experiment.ROOT
SOURCE=ROOT/'test/local-probe-20260917/worker'
OUT=ROOT/'test/local-probe-profile-direct-20260917'
HERE=Path(__file__).parent
replace=experiment.replace
span=experiment.function_span
write=experiment.write
def function(source,name):
a,b,e=span(source,name)
return source[a:e]
def annotate_tail(tail):
"""Fail closed for the reviewed generated model, not a general C parser.
Stage ownership follows extended.py; declarations remain in original scope.
Pipe argument PH inversion and accumulation belong to diagnostics; writes
of diagnostic outputs belong to remaining_outputs.
"""
phase='PF_NODE';mode='node';pipe=False;result=['PF_PROBE(PF_NODE);']
counts={};transitions=[]
for i,line in enumerate(tail.splitlines()):
category=phase
if mode=='node' and re.match(r'w\[\d+\] = [phq]\[\d+\];$',line):mode='port'
if mode=='port' and not re.match(r'w\[\d+\] = [phq]\[\d+\];$',line):mode='mechanical'
if mode=='mechanical' and line.startswith('dy[0]='):mode='gas'
if line.startswith('for(int i=0;i<NSTATES;i++) if(!isfinite'):mode='finite'
if mode=='node':category='PF_NODE'
elif mode=='port':category='PF_PORT'
elif mode=='mechanical':category='PF_MECHANICAL'
elif mode=='finite':category='PF_FINITE'
elif mode=='gas':
if line.startswith('{ double d[4],acc[4]={0};'):pipe=True;category='PF_PIPE'
elif pipe:
if re.match(r'w\[\d+\] = ',line):category='PF_OUTPUTS'
elif line=='}':pipe=False
else:category='PF_PIPE'
elif line.startswith('dy['):category='PF_GAS_EQUATIONS'
elif re.match(r'w\[\d+\] = ',line):category='PF_OUTPUTS'
else:raise AssertionError(('unclassified gas-tail statement',i,line))
if category!=phase:
result.append(f'PF_PROBE({category});');transitions.append((i,category));phase=category
counts[category]=counts.get(category,0)+1;result.append(line)
assert mode=='finite' and not pipe
assert all(counts.get(x,0) for x in ['PF_NODE','PF_PORT','PF_MECHANICAL','PF_GAS_EQUATIONS','PF_OUTPUTS','PF_PIPE','PF_FINITE']),counts
return '\n'.join(result),dict(lineCounts=counts,transitions=transitions)
def instrument_model(source,coarse=False):
clone=function(source,'model_eval_local_internal')
# Keep the original function and append an instrumented duplicate.
clone=clone.replace('model_eval_local_internal(', 'pf_model_eval_local_internal(',1)
begin=clone.index('{')+1;clone=clone[:begin]+'\nPF_PROBE(PF_INIT);'+clone[begin:]
gas=clone.index('if(!(jacobian ? native_jacobian_gas(')
clone=clone[:gas]+'PF_PROBE(PF_GAS_PREP);\n'+clone[gas:]
clone=replace(clone,'if(lp_capture){','PF_PROBE(PF_DISPATCH);\nif(lp_capture){')
clone=clone.replace('lp_snapshot(','pf_snapshot(').replace('lp_save(','pf_save(').replace('lp_reuse(','pf_reuse(')
n=0
def case(m):
nonlocal n;n+=1
return m[0]+f'pf_operation({m[1]});'
clone=re.sub(r'case (\d+):\{',case,clone)
assert n==484 and clone.count('break;}')==484
clone=clone.replace('break;}','PF_MARK(PF_DISPATCH);break;}')
start=clone.index('double node_energy[')
# Leave return 1 and function closing outside stage classification.
end=clone.rindex('return 1;')
tail,meta=annotate_tail(clone[start:end].rstrip())
clone=clone[:start]+tail+'\nPF_PROBE(PF_OTHER);\n'+clone[end:]
wrapper=function(source,'lp_eval').replace('int lp_eval(','int pf_lp_eval(',1)
begin=wrapper.index('{')+1
wrapper=wrapper[:begin]+'\nPF_SCOPE(lp_color+2,lp_color<0?PF_BASELINE:PF_OTHER);pf_eval_count();'+wrapper[begin:]
wrapper=wrapper.replace('lp_begin(t,y);','PF_MARK(PF_SNAPSHOT);lp_begin(t,y);PF_MARK(PF_BASELINE);')
wrapper=wrapper.replace('model_eval_local_internal(t,y,dy,w,1,NULL,NULL,workspace)',
'(pf_coarse?model_eval_local_internal(t,y,dy,w,1,NULL,NULL,workspace):pf_model_eval_local_internal(t,y,dy,w,1,NULL,NULL,workspace))' if coarse
else 'pf_model_eval_local_internal(t,y,dy,w,1,NULL,NULL,workspace)')
# This diagnostic is limited to the all-groups valid local path. Count an
# unexpected full-probe fallback explicitly, rather than misattribute it.
wrapper=replace(wrapper,'int result=eligible && valid ?',
'if(lp_color>=0 && !(eligible && valid))PF_MARK(PF_FULL_FALLBACK);\n int result=eligible && valid ?')
return source+'\n'+clone+'\n'+wrapper+'\n',meta
def instrument_support(source):
reuse=function(source,'lp_reuse').replace('int lp_reuse(','int pf_reuse(',1)
reuse=replace(reuse,'int contextual=lp_before[region]!=lp_after[region];',
'int contextual=lp_before[region]!=lp_after[region];\n if(contextual)PF_MARK(PF_CONTEXT);')
assert reuse.count('{context_misses[lp_color]++;return 0;}')==2
reuse=reuse.replace('{context_misses[lp_color]++;return 0;}',
'{context_misses[lp_color]++;PF_MARK(PF_DISPATCH);return 0;}')
reuse=replace(reuse,'if(contextual){properties->count=after->count;',
'PF_MARK(PF_RESTORE);\n if(contextual){properties->count=after->count;')
reuse=replace(reuse,'return 1;','PF_MARK(PF_DISPATCH);return 1;')
wrappers='''
void pf_snapshot(int index,NativePropertyCache *p,NativePipeCache *c){
if(!needed[index] || captured[index]){lp_snapshot(index,p,c);return;}
PF_MARK(PF_SNAPSHOT);lp_snapshot(index,p,c);PF_MARK(PF_BASELINE);
}
void pf_save(double *p,double *h,double *q,double *w,double *fb){
PF_MARK(PF_SNAPSHOT);lp_save(p,h,q,w,fb);PF_MARK(PF_BASELINE);
}
void lp_jac_time_end(uint64_t start,uint64_t end){jac_ticks+=end-start;jac_calls++;}
'''
return source+'\n'+reuse+'\n'+wrappers
def instrument_cv(source):
rhs=function(source,'jac_rhs_reuse').replace('jac_rhs_reuse(', 'pf_jac_rhs_reuse(',1).replace('lp_eval(', 'pf_lp_eval(')
colored=function(source,'colored_difference').replace('colored_difference(', 'pf_colored_difference(',1).replace('jac_rhs_reuse(', 'pf_jac_rhs_reuse(')
colored=replace(colored,'if (SUNMatZero(matrix)) return -1;',
'PF_SCOPE(0,PF_ZERO);if (SUNMatZero(matrix)) return -1;PF_MARK(PF_OTHER);')
colored=replace(colored,'lp_color=color;memcpy(test,state,NSTATES*sizeof(double));',
'PF_SCOPE(0,PF_PERTURB);lp_color=color;memcpy(test,state,NSTATES*sizeof(double));')
colored=replace(colored,'#if MODEL_JACOBIAN_GAS_REUSE', 'PF_MARK(PF_OTHER);\n#if MODEL_JACOBIAN_GAS_REUSE')
colored=replace(colored,'if (flag) return flag;','PF_SCOPE(0,PF_ASSEMBLY);if (flag) return flag;')
colored=replace(colored,' }\n context->run->jacobian_colored_evals++;',
' PF_MARK(PF_OTHER);\n }\n context->run->jacobian_colored_evals++;')
jac=function(source,'cv_jacobian_original').replace('cv_jacobian_original(', 'pf_cv_jacobian_original(',1).replace('jac_rhs_reuse(', 'pf_jac_rhs_reuse(').replace('colored_difference(', 'pf_colored_difference(')
jac=replace(jac,'int flag=jac_increments(context,y,fy,increments);',
'PF_SCOPE(0,PF_INCREMENT);int flag=jac_increments(context,y,fy,increments);PF_MARK(PF_OTHER);')
jac=replace(jac,'fy=tmp3;', 'PF_SCOPE(0,PF_OTHER);fy=tmp3;')
a,b,e=span(source,'cv_jacobian');sig=source[a:b]
wrapper=sig+'''{
lp_color=-1;uint64_t start=lp_tick();int selected=pf_select();
if(selected)pf_open();
int result=selected?pf_cv_jacobian_original(t,y,fy,matrix,user,tmp1,tmp2,tmp3):cv_jacobian_original(t,y,fy,matrix,user,tmp1,tmp2,tmp3);
if(selected)pf_close();
uint64_t finish=lp_tick();lp_jac_time_end(start,finish);
if(selected)pf_flush(start,finish);
if(!result){lp_matrix(t,N_VGetArrayPointer(y),SUNDenseMatrix_Data(matrix));pf_validate_matrix(t,N_VGetArrayPointer(y),SUNDenseMatrix_Data(matrix));}
return result;
}'''
return source[:a]+rhs+'\n'+colored+'\n'+jac+'\n'+wrapper+source[e:]
def prepare(coarse=False):
OUT.mkdir(exist_ok=True);work=OUT/('coarse-worker' if coarse else 'worker');work.mkdir(exist_ok=True)
sources={p.name:p.read_text(encoding='utf-8') for p in SOURCE.glob('*.c')}
before={k:hashlib.sha256(v.encode()).hexdigest() for k,v in sources.items()}
sources['model.c'],boundaries=instrument_model(sources['model.c'],coarse)
sources['local_probe_support.c']=instrument_support(sources['local_probe_support.c'])
sources['cvode_solver.c']=instrument_cv(sources['cvode_solver.c'])
sources['common.c']=replace(sources['common.c'],'lp_start();','lp_start();pf_initialize();')
sources['common.c']=replace(sources['common.c'],'lp_finish();','lp_finish();pf_finish();')
plan=json.loads((SOURCE.parent/'plan.json').read_text(encoding='utf-8'))
affected=[[int(i in set(g['affectedOperations'])) for i in plan['order']] for g in plan['groups']]
tables='const unsigned char pf_affected[LP_NC][LP_NO]={'+','.join('{'+','.join(map(str,a))+'}' for a in affected)+'};'
sources['local_probe_profile.c']=(HERE/'local_probe_profile.c').read_text(encoding='utf-8').replace('/* PROFILE_TABLES */',tables)
for name in ('model.h','local_probe.h'):shutil.copyfile(SOURCE/name,work/name)
shutil.copyfile(HERE/'local_probe_profile.h',work/'local_probe_profile.h')
cc,sun,_=experiment.builder.toolchain();flags,libs,dlls,exe=experiment.builder.platform_build_inputs(sun);flags+=['-DLP_OBSERVE=0']
started=time.perf_counter()
def compile_one(item):
i,(name,src)=item;path=work/name;path.write_text('#include "local_probe_profile.h"\n'+src,encoding='utf-8',newline='\n');obj=work/f'profile-{i}.o';log=[]
experiment.builder._command([cc,*flags,'-I',str(work),'-I',str(experiment.builder.NATIVE/'include'),'-I',str(sun/'include'),'-c',str(path),'-o',str(obj)],log=log,timeout=180)
return obj,log
with ThreadPoolExecutor(max_workers=4) as pool:objects=list(pool.map(compile_one,enumerate(sources.items())))
log=[];experiment.builder._command([cc,*flags,*[str(o) for o,_ in objects],*experiment.builder.link_library_arguments(libs),'-lm','-o',str(work/exe)],log=log)
for dll in dlls:shutil.copyfile(dll,work/dll.name)
(work/'build.log').write_text('\n'.join(sum([x for _,x in objects],[])+log),encoding='utf-8')
write(OUT/('build-coarse.json' if coarse else 'build.json'),dict(sourceHashes=before,sourceDirectory=str(SOURCE),boundaries=boundaries,seconds=time.perf_counter()-started))
print('BUILT',time.perf_counter()-started,flush=True)
def run(label,stride=16,offset=0,control=False,matrices=False,coarse=False):
work=OUT/label;work.mkdir(exist_ok=True)
executable=(SOURCE if control else OUT/('coarse-worker' if coarse else 'worker'))/'model.exe'
env=os.environ.copy();env.update(LOCAL_PROBE_MASK='0x7ffffff',PROBE_PROFILE_STRIDE=str(stride),PROBE_PROFILE_OFFSET=str(offset),PROBE_PROFILE_MATRICES=str(int(matrices)),PROBE_PROFILE_COARSE=str(int(coarse)))
args=[str(executable),'--method','BDF','--start','0','--stop','10','--sample-step','.01','--max-step','1e30','--rtol','1e-8','--timeout','300',
'--sample-file',str(work/'states.bin'),'--output-block-file',str(work/'outputs.bin'),'--output',str(work/'result.json')]
start=time.perf_counter()
with (work/'stderr.log').open('wb') as f:
p=subprocess.run(args,cwd=work,env=env,stdout=subprocess.PIPE,stderr=f,timeout=330,creationflags=subprocess.CREATE_NO_WINDOW)
elapsed=time.perf_counter()-start
if p.returncode:raise RuntimeError((label,p.returncode,(work/'stderr.log').read_text()[-4000:]))
r=json.loads((work/'result.json').read_text());diag=json.loads((work/'probe.json').read_text())
fields=('success','finalState','final','propertyWarnings','acceptedSteps','rejectedSteps','stateTransitions','solverStarts','nfev','njev','nlu','solveSeconds','solveCpuSeconds')
record={k:r[k] for k in fields}
for name in ('states','outputs','events','jacobians'):
path=work/f'{name}.bin'
if path.exists():
with path.open('rb') as f:record[name+'Sha256']=hashlib.file_digest(f,'sha256').hexdigest()
record.update(label=label,stride=stride,offset=offset,control=control,matrices=matrices,coarse=coarse,processSeconds=elapsed,diagnostic=diag)
write(work/'measurement.json',record)
# Numerical gate on every run, including the sampling-density controls.
reference=json.loads((SOURCE.parent/'all-run-0/measurement.json').read_text(encoding='utf-8'))
exact=['statesSha256','outputsSha256','eventsSha256','finalState','final','propertyWarnings','acceptedSteps','rejectedSteps','stateTransitions','solverStarts','nfev','njev','nlu']
differences=[k for k in exact if record[k]!=reference[k]]
differences += [k for k in ('newtonIterations','newtonConvergenceFailures','contextComparedBytes','contextCopiedBytes','modelCalls','groups') if diag[k]!=reference['diagnostic'][k]]
assert not differences,(label,differences)
if matrices:
ref=SOURCE.parent/'all-audit/jacobians.bin'
with ref.open('rb') as f:assert record['jacobiansSha256']==hashlib.file_digest(f,'sha256').hexdigest()
print('RUN',label,'Jacobian',diag['jacobianSeconds'],'solve',r['solveSeconds'],'CPU',r['solveCpuSeconds'],'process',elapsed,'exact OK',flush=True)
return record
def batch():
run('control-warmup',control=True)
run('profile-warmup',16,101)
for i in range(5):
jobs=[(f'control-{i}',0,0,True),(f'profile-{i}',16,i+17,False)]
if i%2:jobs.reverse()
for label,stride,offset,control in jobs:run(label,stride,offset,control)
for i in range(2):
run(f'disabled-{i}',0,i)
run(f'density8-{i}',8,i+37)
run(f'density32-{i}',32,i+71)
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('action',choices=['prepare','run','batch']);p.add_argument('--label',default='sample');p.add_argument('--stride',type=int,default=16);p.add_argument('--offset',type=int,default=0);p.add_argument('--control',action='store_true');p.add_argument('--matrices',action='store_true');p.add_argument('--coarse',action='store_true');a=p.parse_args()
if a.action=='prepare':prepare(a.coarse)
elif a.action=='batch':batch()
else:run(a.label,a.stride,a.offset,a.control,a.matrices,a.coarse)