在单次 Jacobian 构建内按完整输入精确复用储气物性、PH 反算、密度和管路求根结果,保持原有求值副作用、差分政策与失败回退。八路模型求解 CPU 中位数减少 19.27%,循环和不循环的完整原始采样均与恢复基线一致。 增加独立的跨平台时间剖析工具,记录互斥阶段耗时、Newton/LU 统计、矩阵复用与内核复用,保存 UD00 两种工况的调查报告和机器可读汇总。 补充 Windows/Linux 运行、测试、原生编译和剖析所需依赖文档及索引,不修改依赖清单、版本锁或安装环境。 验证:8 项新增专项回归通过;2270 次完整 Jacobian 核对零差异;16 次剖析配对及预热运行保持完整数值一致。既有固定样本哈希失败和 Linux 实机验收限制见报告。
133 lines
7.2 KiB
Python
133 lines
7.2 KiB
Python
"""Freeze native workers and compare exact outputs before/after Jacobian reuse.
|
|
|
|
Use prepare --name before before editing production sources. Then prepare after.
|
|
Run measured workers serially, outside compilation or verification activity.
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import platform
|
|
import shutil
|
|
import statistics
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
ROOT=Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0,str(ROOT))
|
|
from app.main import compile_system_xml_network
|
|
from app.simulation.native_codegen.build import build_native
|
|
from app.simulation.native_codegen.compiler import compile_native_program
|
|
from app.simulation.native_codegen.input import load_input
|
|
|
|
OUT=ROOT/'test/jacobian-reuse-20260915'
|
|
MODEL=ROOT/'tests/data/test-mql-8-corrected.json'
|
|
EXACT=('statesSha256','outputsSha256','finalState','final','propertyWarnings',
|
|
'nfev','acceptedSteps','rejectedSteps','njev','nlu','stateTransitions','solverStarts')
|
|
|
|
|
|
def digest(path):
|
|
with path.open('rb') as stream:
|
|
return hashlib.file_digest(stream,'sha256').hexdigest()
|
|
|
|
|
|
def write(path,data):
|
|
path.write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
|
|
|
|
|
|
def prepare(name,cyclic):
|
|
variant=MODEL
|
|
if cyclic:
|
|
project=json.loads(MODEL.read_text(encoding='utf-8'))
|
|
for node in project['nodes']:
|
|
if node['data'].get('modelType')=='amesim_ud00':
|
|
node['data']['parameters']['iscyclic']=1
|
|
project['simulation']['t_stop']=21.7
|
|
variant=OUT/'cyclic-project.json'
|
|
write(variant,project)
|
|
_,document=load_input(variant)
|
|
program=compile_native_program(compile_system_xml_network(document))
|
|
build=build_native(program)
|
|
try:
|
|
target=OUT/(name+('-cyclic' if cyclic else ''))
|
|
shutil.copytree(build.executable.parent,target)
|
|
sources=list((ROOT/'app/simulation/native_codegen').glob('*.py'))+list((ROOT/'native').rglob('*.c'))+list((ROOT/'native/include').glob('*.h'))
|
|
write(target/'comparison-metadata.json',dict(buildKey=build.manifest['buildKey'],
|
|
inputSha256=digest(variant),sourceHashes={p.relative_to(ROOT).as_posix():digest(p) for p in sources},
|
|
platform=platform.platform(),buildSeconds=build.seconds,details=build.details,
|
|
sourceRevision=subprocess.check_output(['git','rev-parse','HEAD'],cwd=ROOT,text=True).strip()))
|
|
print(json.dumps(dict(build=target.name,key=build.manifest['buildKey'],seconds=build.seconds)),flush=True)
|
|
finally:
|
|
build.close()
|
|
|
|
|
|
def run(name,label,cyclic,stop,verify=False):
|
|
build=OUT/(name+('-cyclic' if cyclic else ''))
|
|
target=OUT/(name+('-cyclic-' if cyclic else '-')+label)
|
|
target.mkdir()
|
|
command=[str(build/('model.exe' if sys.platform=='win32' else 'model')),
|
|
'--method','BDF','--start','0','--stop',str(stop),'--sample-step','.01',
|
|
'--rtol','1e-8','--max-step','1e30','--timeout','300',
|
|
'--sample-file',str(target/'states.bin'),'--output-block-file',str(target/'outputs.bin'),
|
|
'--output',str(target/'result.json')]
|
|
if verify: command.append('--verify-jacobian')
|
|
start=time.perf_counter()
|
|
with (target/'stderr.log').open('w',encoding='utf-8') as log:
|
|
process=subprocess.run(command,cwd=build,stdout=subprocess.DEVNULL,stderr=log,timeout=330)
|
|
elapsed=time.perf_counter()-start
|
|
result=json.loads((target/'result.json').read_text(encoding='utf-8'))
|
|
assert process.returncode==0 and result['success'],result.get('message')
|
|
record={k:result[k] for k in EXACT if k not in ('statesSha256','outputsSha256')}
|
|
record.update(name=name,label=label,cyclic=cyclic,stop=stop,verify=verify,
|
|
processWallSeconds=elapsed,solveSeconds=result['solveSeconds'],solveCpuSeconds=result['solveCpuSeconds'],
|
|
statesSha256=digest(target/'states.bin'),outputsSha256=digest(target/'outputs.bin'),
|
|
jacobianChecks=result['jacobianChecks'],jacobianMismatches=result['jacobianMismatches'],
|
|
jacobianReuse=result.get('jacobianReuse'),sampleCount=len(result['series']['time']))
|
|
write(target/'measurement.json',record)
|
|
print(json.dumps({k:v for k,v in record.items() if k not in ('finalState','final','propertyWarnings')}),flush=True)
|
|
|
|
|
|
def report():
|
|
records=[json.loads(p.read_text(encoding='utf-8')) for p in sorted(OUT.glob('*-run-*/measurement.json'))]
|
|
assert len(records)>=6
|
|
checks=[dict(run=r['name']+'-'+r['label'],differences=[k for k in EXACT if r[k]!=records[0][k]]) for r in records]
|
|
medians={name:{key:statistics.median(r[key] for r in records if r['name']==name)
|
|
for key in ('solveCpuSeconds','solveSeconds','processWallSeconds')} for name in ('before','after')}
|
|
cases=[]
|
|
for cyclic,suffix in ((False,'noncyclic-check'),(True,'cyclic-check')):
|
|
pair=[json.loads((OUT/f'{name}-{suffix}'/'measurement.json').read_text(encoding='utf-8')) for name in ('before','after')]
|
|
case=dict(cyclic=cyclic,stop=21.7,sampleCount=pair[0]['sampleCount'],
|
|
differences=[k for k in EXACT if pair[0][k]!=pair[1][k]],
|
|
measurements=[{k:v for k,v in r.items() if k not in ('finalState','final','propertyWarnings')} for r in pair])
|
|
cases.append(case)
|
|
verification=[]
|
|
for suffix,reference_suffix in (('verify-check','run-0'),('cyclic-verify-check','cyclic-check')):
|
|
checked=json.loads((OUT/f'after-{suffix}'/'measurement.json').read_text(encoding='utf-8'))
|
|
reference=json.loads((OUT/f'after-{reference_suffix}'/'measurement.json').read_text(encoding='utf-8'))
|
|
differences=[k for k in EXACT if k!='nfev' and checked[k]!=reference[k]]
|
|
assert checked['jacobianMismatches']==0 and checked['jacobianChecks']==reference['njev']
|
|
assert checked['nfev']==reference['nfev']+checked['jacobianChecks']*len(reference['finalState'])
|
|
assert checked['jacobianReuse']==reference['jacobianReuse']
|
|
verification.append(dict(run=suffix,checks=checked['jacobianChecks'],mismatches=checked['jacobianMismatches'],differences=differences))
|
|
summary=dict(checks=checks,medians=medians,ud00=cases,verification=verification,measurements=records,
|
|
scope='One warmup per build excluded. Three alternating serial measurements at 0-10s; UD00 modes separately compared at 0-21.7s. Full native worker with raw samples; excludes build/API/browser.')
|
|
write(OUT/'comparison.json',summary)
|
|
print(json.dumps(dict(checks=checks,medians=medians,ud00=cases,verification=verification),indent=2))
|
|
assert not any(c['differences'] for c in checks+cases+verification),'Numerical comparison failed'
|
|
|
|
|
|
if __name__=='__main__':
|
|
parser=argparse.ArgumentParser()
|
|
parser.add_argument('action',choices=('prepare','run','report'))
|
|
parser.add_argument('--name',choices=('before','after'),default='before')
|
|
parser.add_argument('--label',default='warmup')
|
|
parser.add_argument('--cyclic',action='store_true')
|
|
parser.add_argument('--verify',action='store_true')
|
|
parser.add_argument('--stop',type=float,default=10)
|
|
args=parser.parse_args()
|
|
OUT.mkdir(parents=True,exist_ok=True)
|
|
if args.action=='prepare': prepare(args.name,args.cyclic)
|
|
elif args.action=='run': run(args.name,args.label,args.cyclic,args.stop,args.verify)
|
|
else: report()
|