"""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()