"""Verify production LSTP integration and probe force at real Amesim timestamps. Dense trace sampling operates on an isolated runtime copy, without changing the step schedule or time-event treatment. It never interpolates across events. """ from concurrent.futures import ThreadPoolExecutor from dataclasses import replace import argparse import hashlib import json import math import os from pathlib import Path import re import shutil import subprocess import sys from unittest.mock import patch import numpy as np ROOT=Path(__file__).resolve().parents[2] sys.path.insert(0,str(ROOT)) from app.main import compile_system_xml_network from app.simulation.backends import simulation_config from app.simulation.native_codegen import build as builder,result_storage from app.simulation.native_codegen.compiler import compile_native_program from app.simulation.native_codegen.input import load_input from app.simulation.native_codegen.runner import execute_native from tests.manual import evaluate_mql8_correctness as evaluation,mql8_comparison as curves PREVIOUS=ROOT/'test/mechanical-events-20260917/baselines' DEFAULT=ROOT/'test/lstp-mainline-20260917' save=evaluation.save def model(profile): project=ROOT/'test/output-semantics-20260917/after'/profile/'platform.json' xml,doc=load_input(project) net=compile_system_xml_network(doc) return project,xml,doc,net,compile_native_program(net) def build(program,out,native=None): with patch.object(builder,'CACHE',out/'cache'),patch.object(builder,'NATIVE',native or ROOT/'native'), \ patch.object(builder,'ThreadPoolExecutor',lambda **kw:ThreadPoolExecutor(max_workers=1)): return builder.build_native(program) def baseline(out): results={} for profile in ('full','noncyclic'): directory=out/'baseline'/profile if (directory/'summary.json').exists(): results[profile]=json.loads((directory/'summary.json').read_bytes());continue directory.mkdir(parents=True,exist_ok=True) project,xml,doc,net,program=model(profile) old=json.loads((PREVIOUS/profile/'lstp/event-descriptors.json').read_bytes()) # The sole generated-source addition is a constant descriptor table. original=re.sub(r'^const NativeContact model_contacts\[[^\n]+\n','',program.source,flags=re.M) assert hashlib.sha256(original.encode()).hexdigest()==old['originalModelSourceSha256'] assert program.jacobian_structure==old['jacobianStructure'] (directory/'platform.json').write_bytes(project.read_bytes());(directory/'platform.xml').write_bytes(xml) reference=directory/'amesim';reference.mkdir(exist_ok=True) for filename in ('test_mql_.var','test_mql_.results'): if not (reference/filename).exists():os.link(PREVIOUS/profile/'reference'/filename,reference/filename) audit=json.loads((PREVIOUS/profile/'audit/audit.json').read_bytes()) print(profile,'production build',flush=True) compiled=build(program,out) try: with patch.object(result_storage,'RESULT_ROOT',out/'results'): r=execute_native(compiled,replace(simulation_config(doc.simulation),rtol=1e-8),.01, run_dir=directory/'native',timeout=180) assert r['success'],r['message'] summary=evaluation.compare(directory,json.loads(project.read_bytes()),net,audit,evaluation.PROFILES[profile]) summary['nativeRun']={k:v for k,v in r.items() if k not in ('series','final','finalState')} with np.load(directory/'curves.npz') as current,np.load(PREVIOUS/profile/'lstp/curves.npz') as expected: summary['allComparedArraysEqualIsolatedLstp']=all(np.array_equal(current[k],expected[k],equal_nan=True) for k in current.files) summary['generatedRhsUnchanged']=True save(directory/'summary.json',summary);results[profile]=summary print(profile,'done',summary['allComparedArraysEqualIsolatedLstp'],r['contactEvents'],flush=True) finally:compiled.close() save(out/'baseline-summary.json',results) def trace(out): project,xml,doc,net,program=model('full') audit=json.loads((PREVIOUS/'full/audit/audit.json').read_bytes());curves.configure(audit,net) ame=curves.read_ame(PREVIOUS/'full/reference') at=np.array(ame.times) # Query BEFORE and AFTER every forcing boundary, including the exact saved # Amesim row. These queries never become solver stop times. queries=set();points=[] boundaries=evaluation.event_times(json.loads(project.read_bytes()),50) for boundary in boundaries: if boundary<=0:continue ai=int(np.argmin(abs(at-boundary))) points.append(dict(boundary=boundary,ameIndex=ai,ameTime=float(at[ai]))) queries.update((math.nextafter(boundary,-math.inf),boundary,float(at[ai]))) for delta in (-1e-6,-1e-9,-1e-12,1e-14,1e-13,5e-13,1e-12,2e-12,3e-12,1e-11,1e-10,1e-9,1e-8,1e-7,1e-6,1e-5,1e-4,.001): queries.add(boundary+delta) queries=sorted(t for t in queries if 032.39999)&(times<32.4004)) rows=[dict(time=float(times[i]),**{k:float(v[i]) for k,v in data.items()}) for i in selected] j=int(np.argmax(abs(data['force']))) peak=dict(index=j,time=float(times[j]),**{k:float(v[j]) for k,v in data.items()}) save(directory/'mechanical-events.json',dict(run=run,samples=len(times),around324=rows,maximumAbsoluteForce=peak)) print('Amesim event output',len(times),'rows',rows,'peak',peak,flush=True) def event_comparison(out): directory=out/'event-output-comparison';directory.mkdir(exist_ok=True) (directory/'native').mkdir(exist_ok=True);(directory/'amesim').mkdir(exist_ok=True) for source,target in [(out/'baseline/full/native/result.json',directory/'native/result.json'), *[(out/'amesim-event-output'/name,directory/'amesim'/name) for name in ('test_mql_.var','test_mql_.results')]]: if not target.exists():os.link(source,target) project,_,_,net,_=model('full') audit=json.loads((PREVIOUS/'full/audit/audit.json').read_bytes()) original_reference=curves.read_ame(directory/'amesim') times=np.array(original_reference.times);order=np.argsort(times,kind='stable') # Amesim writes its .8 s discontinuity after an already-written grid row # at .8000000000000009. Preserve every row/value and stable-sort by its # actual timestamp for this diagnostic matcher; retain the permutation. sorted_reference=replace(original_reference,times=tuple(times[order]), series_by_data_path={k:tuple(np.asarray(v)[order]) for k,v in original_reference.series_by_data_path.items()}) save(directory/'ame-row-order.json',dict(originalTimeInversions=np.flatnonzero(np.diff(times)<0).tolist(),sortedToOriginal=order.tolist())) with patch.object(curves,'read_ame',return_value=sorted_reference): summary=evaluation.compare(directory,json.loads(project.read_bytes()),net,audit,evaluation.PROFILES['full']) # Adding event outputs must preserve every original Amesim saved sample. old=curves.read_ame(PREVIOUS/'full/reference');new=sorted_reference oldtimes=np.array(old.times);newtimes=np.array(new.times) indices=np.searchsorted(newtimes,oldtimes) assert np.array_equal(newtimes[indices],oldtimes) unchanged=all(np.array_equal(np.array(new.series(key))[indices],values) for key,values in old.series_by_data_path.items()) assert unchanged,'Amesim event printout changed original results' summary['allOriginalAmesimRowsUnchanged']=unchanged save(directory/'summary.json',summary) print('event-output comparison',summary['groups']['force']['worstAbsolute'], 'above5',summary['above5PercentCount'],'unpaired',summary['phaseUnpairedGridCount'],flush=True) def main(): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--output',type=Path,default=DEFAULT) parser.add_argument('--stage',choices=('baseline','trace','rebase','analyze','amesim-events','event-comparison','all'),default='all') args=parser.parse_args();out=args.output.resolve();out.mkdir(parents=True,exist_ok=True) if args.stage in ('baseline','all'):baseline(out) if args.stage in ('trace','all'):trace(out) if args.stage in ('rebase','all'):rebase(out) if args.stage in ('analyze','all'):analyze(out) if args.stage in ('amesim-events','all'):amesim_events(out) if args.stage in ('event-comparison','all'):event_comparison(out) if __name__=='__main__':main()