优化 Jacobian 确定性复用并补充性能剖析与平台依赖文档

在单次 Jacobian 构建内按完整输入精确复用储气物性、PH 反算、密度和管路求根结果,保持原有求值副作用、差分政策与失败回退。八路模型求解 CPU 中位数减少 19.27%,循环和不循环的完整原始采样均与恢复基线一致。

增加独立的跨平台时间剖析工具,记录互斥阶段耗时、Newton/LU 统计、矩阵复用与内核复用,保存 UD00 两种工况的调查报告和机器可读汇总。

补充 Windows/Linux 运行、测试、原生编译和剖析所需依赖文档及索引,不修改依赖清单、版本锁或安装环境。

验证:8 项新增专项回归通过;2270 次完整 Jacobian 核对零差异;16 次剖析配对及预热运行保持完整数值一致。既有固定样本哈希失败和 Linux 实机验收限制见报告。
This commit is contained in:
ljz committed 2026-09-16 13:39:53 +08:00
1 parent 2b07d996cf
commit 1aac220084
26 files changed
+2293 -13

No files matched your search

@@ -0,0 +1,132 @@
"""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()
+343
View File
@@ -0,0 +1,343 @@
"""Profile the current native BDF solver in an isolated diagnostic source copy.
Example: python tests/manual/profile_native_solver.py --run
Builds BOTH UD00 modes before timing; one excluded warmup and three serial pairs
per mode by default. Production sources and cached workers are not instrumented.
Requires the production GCC toolchain and SUNDIALS 7.4 public interfaces.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import math
import os
from pathlib import Path
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, toolchain, platform_build_inputs, link_library_arguments, _command,
RUNTIME_SOURCES,
)
from app.simulation.native_codegen.compiler import compile_native_program
from app.simulation.native_codegen.input import load_input
from app.simulation.native_codegen.modules import component_modules
from tests.manual.native_compute_profile import replace_once, scope_function
HERE = Path(__file__).resolve().parent
APIS = {
'STEPS': 'CVodeGetNumSteps', 'RHS': 'CVodeGetNumRhsEvals',
'LINEAR_RHS': 'CVodeGetNumLinRhsEvals', 'JACOBIANS': 'CVodeGetNumJacEvals',
'SETUPS': 'CVodeGetNumLinSolvSetups', 'ERROR_FAILS': 'CVodeGetNumErrTestFails',
'NEWTON_ITERS': 'CVodeGetNumNonlinSolvIters',
'NEWTON_FAILS': 'CVodeGetNumNonlinSolvConvFails',
'STEP_SOLVE_FAILS': 'CVodeGetNumStepSolveFails',
}
# Every exclusive scope occurs exactly once in this disjoint partition.
PARTITION = {
'residual_evaluation': ['rhs', 'nonlinear_residual'],
'jacobian_assembly': ['jacobian', 'jacobian_probe'],
'numerical_factorization': ['numerical_factorization'],
'linear_solve': ['linear_solve'],
'linear_system_setup_other': ['linear_setup'],
'newton_overhead': ['newton'],
'cvode_controller_including_error_estimation': ['cvode_controller'],
'event_detection': ['event_detection'],
'accepted_property_checks': ['accepted_property_checks'],
'sampling_and_storage': ['sampling', 'append', 'flush'],
'output_replay': ['output_replay'],
'result_encoding': ['result_encoding'],
'poll_and_progress': ['poll'],
'framework_other': ['total', 'integration'],
}
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 scope(text, function, category):
return scope_function(text, function, category).replace('PROFILE_' + category.upper(), 'P_' + category.upper())
def instrument(native):
for name in ('solver_profile.h', 'solver_profile_hooks.h'):
shutil.copy2(HERE / name, native / 'include' / name)
shutil.copy2(HERE / 'solver_profile.c', native / 'runtime/solver_profile.c')
for filename, functions in {
'common.c': {'native_poll': 'poll', 'native_accept': 'event',
'native_append': 'append', 'check_property_temperatures': 'property'},
'cvode_solver.c': {'native_bdf': 'integration', 'cv_rhs': 'rhs',
'cv_jacobian': 'jacobian', 'jac_rhs': 'jacobian_probe'},
'sample_storage.c': {'native_samples_flush': 'flush', 'native_samples_outputs': 'replay'},
'main.c': {'main': 'total', 'write_result': 'encoding'},
}.items():
path = native / 'runtime' / filename
text = '#include "solver_profile.h"\n' + path.read_text(encoding='utf-8')
for name, category in functions.items():
text = scope(text, name, category)
if filename == 'common.c':
text = replace_once(text, ' if (r->options.record_samples) {',
' if (r->options.record_samples) {\n PROFILE_SCOPE(P_SAMPLING);')
text = replace_once(text, ' return model_friction_drives(t,state,drives);',
' profile_add(C_FRICTION_EVALS,0,1);\n return model_friction_drives(t,state,drives);')
elif filename == 'cvode_solver.c':
text = replace_once(text, 'typedef struct { void *solver;',
'#include "solver_profile_hooks.h"\ntypedef struct { void *solver;')
text = replace_once(text, ' if(!context->jacobian_workspace)return jac_rhs(context,t,y,f);',
' if(!context->jacobian_workspace)return jac_rhs(context,t,y,f);\n PROFILE_SCOPE(P_JACOBIAN_PROBE);')
text = replace_once(text, ' int success=0, initialized=0;',
' SUNNonlinearSolver profile_nls=NULL;\n int success=0, initialized=0;')
text = replace_once(text, ' if (!linear) goto allocation_failure;',
' if (!linear) goto allocation_failure;\n'
' original_lu=linear->ops->setup;original_solve=linear->ops->solve;\n'
' linear->ops->setup=profiled_lu;linear->ops->solve=profiled_solve;')
text = replace_once(text, ' CV_CHECK(CVodeSetMaxStep(solver,r->options.max_step));',
' CV_CHECK(CVodeSetMaxStep(solver,r->options.max_step));\n'
' profile_nls=profile_attach_newton(y,ctx);\n'
' allocation="SUNNonlinSol_Newton (profile)";\n'
' if(!profile_nls)goto allocation_failure;\n'
' CV_CHECK(CVodeSetNonlinearSolver(solver,profile_nls));')
text = replace_once(text, ' if (linear) SUNLinSolFree(linear);',
' if(profile_nls)SUNNonlinSolFree(profile_nls);\n if (linear) SUNLinSolFree(linear);')
text = replace_once(text, 'int flag=CVode(solver,end,y,&next,CV_ONE_STEP);',
'int flag=profiled_cvode(solver,end,y,&next,CV_ONE_STEP);')
text = replace_once(text, ' if (t<r->options.stop) {',
' if (t<r->options.stop) {\n profile_add(C_BOUNDARIES,0,1);')
snapshots = '\n profile_add(C_SEGMENTS,0,1);\n'
for tag, api in APIS.items():
snapshots += f' {{long int n=0;int code={api}(solver,&n);profile_add(C_{tag},code,n);}}\n'
text = replace_once(text, 'static void counters(NativeRun *r, void *solver) {',
'static void counters(NativeRun *r, void *solver) {' + snapshots)
elif filename == 'main.c':
text = replace_once(text, ' native_run_free(&r); return code;',
' native_run_free(&r); profile_end(&profile_scope);profile_dump();return code;')
path.write_text(text, encoding='utf-8', newline='\n')
def prepare(args):
out = args.output.resolve()
if not out.is_relative_to(ROOT / 'test') or out.exists():
raise ValueError('Use a NEW output directory beneath the project test/ directory')
if args.repeats < 1 or args.warmups < 0:
raise ValueError('repeats must be positive and warmups nonnegative')
out.mkdir(parents=True)
compiler, sundials, compiler_version = toolchain()
flags, libraries, dlls, executable = platform_build_inputs(sundials)
project = json.loads(args.input.read_text(encoding='utf-8'))
variants = []
for cyclic in (False, True):
case = out / ('cyclic' if cyclic else 'noncyclic')
case.mkdir()
ud00 = [n for n in project['nodes'] if n['data'].get('modelType') == 'amesim_ud00']
if not ud00:
raise ValueError('UD00 comparison requires at least one UD00 signal')
for node in ud00:
node['data']['parameters']['iscyclic'] = int(cyclic)
project['simulation']['t_stop'] = args.stop
write(case / 'input.json', project)
_, document = load_input(case / 'input.json')
program = compile_native_program(compile_system_xml_network(document))
# The assembly timer observes our callback; CVODE's private dense-DQ
# fallback has no separate public timer. Reject instead of mislabelling.
if '#define MODEL_JACOBIAN_COLORED 1' not in program.header:
raise ValueError('This diagnostic currently requires generated colored Jacobian support')
build = build_native(program)
try:
shutil.copytree(build.executable.parent, case / 'control')
manifest = build.manifest
finally:
build.close()
native = case / 'native'
shutil.copytree(ROOT / 'native', native)
target = case / 'profiled'
target.mkdir()
for name in ('model.c', 'model.h', 'THIRD_PARTY_NOTICES.txt'):
shutil.copy2(case / 'control' / name, target / name)
for dll in dlls:
shutil.copy2(dll, target / dll.name)
instrument(native)
sources = [native / name for name in RUNTIME_SOURCES]
sources += [native / 'components/modules' / f'{name}.c'
for name in component_modules(program.source + '\n' + program.header)]
sources.append(native / 'runtime/solver_profile.c')
command = [compiler, *flags, '-I', str(target), '-I', str(native / 'include'),
'-I', str(sundials / 'include'), str(target / 'model.c'), *map(str, sources),
*link_library_arguments(libraries), '-lm', '-o', str(target / executable)]
log = []
try:
_command(command, log=log, timeout=180)
finally:
(case / 'build.log').write_text('\n'.join(log), encoding='utf-8')
assert digest(target / 'model.c') == digest(case / 'control/model.c')
variants.append(dict(name=case.name, ud00Count=len(ud00), buildKey=manifest['buildKey'],
executable=executable, inputSha256=digest(case / 'input.json'),
controlSha256=digest(case / 'control' / executable),
profiledSha256=digest(target / executable),
nativeSourceHashes={p.relative_to(ROOT / 'native').as_posix(): digest(p)
for p in (ROOT / 'native').rglob('*') if p.is_file()},
instrumentedHashes={p.relative_to(native).as_posix(): digest(p)
for p in native.rglob('*') if p.is_file()}))
print(f'Prepared {case.name}: {manifest["buildKey"]}', flush=True)
result = dict(variants=variants, stop=args.stop, repeats=args.repeats, warmups=args.warmups,
compiler=compiler_version, platform=sys.platform,
revision=subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip(),
settings=dict(method='BDF', start=0, sample_step=.01, rtol=1e-8, max_step=1e30),
scope='Native worker: model initialization through result encoding and cleanup. '
'Excludes build, process launch, API and browser. Error estimation remains inside CVODE controller. '
'Dense LU has no symbolic factorization. Explicit Newton uses the same library implementation.')
write(out / 'prepared.json', result)
return result
def analyze(profile, result):
assert profile['counterErrors'] == 0
s, c = profile['scopes'], profile['counters']
assert set(s) == {k for group in PARTITION.values() for k in group}
times = {name: sum(s[k]['exclusiveSeconds'] for k in scopes) for name, scopes in PARTITION.items()}
total = s['total']['inclusiveSeconds']
assert math.isclose(sum(times.values()), total, abs_tol=1e-8, rel_tol=1e-10)
assert all(t >= 0 for t in times.values())
for name, key in (('residual_evaluations', 'cvodeRhsCalls'), ('linear_rhs_evaluations', 'cvodeLinearRhsCalls'),
('jacobian_evaluations', 'njev'), ('linear_setups', 'nlu'),
('error_test_failures', 'rejectedSteps'), ('counter_segments', 'solverStarts')):
assert c[name] == result[key], (name, c[name], result[key])
assert s['jacobian']['calls'] == c['jacobian_evaluations']
assert s['numerical_factorization']['calls'] == c['linear_setups']
assert c['jacobian_reuses'] + c['jacobian_refresh_setups'] + c['linear_setup_failures'] == c['linear_setups']
assert s['rhs']['calls'] == c['residual_evaluations'] + c['linear_rhs_evaluations']
assert result['nfev'] == s['rhs']['calls'] + result['jacobianRhsCalls'] + c['event_model_evaluations']
c.update(rejected_steps=c['error_test_failures'] + c['nonlinear_step_failures'],
application_accepted_steps=result['acceptedSteps'],
same_time_returns=result['solverControl']['sameTimeReturns'],
max_same_time_streak=result['solverControl']['maxSameTimeStreak'],
nonlinear_residual_calls=s['nonlinear_residual']['calls'],
jacobian_probe_evaluations=result['jacobianRhsCalls'],
LU_factorizations=s['numerical_factorization']['calls'], LU_solves=s['linear_solve']['calls'],
newton_solve_calls=s['newton']['calls'], event_count=result['stateTransitions'],
event_detection_calls=s['event_detection']['calls'], solver_starts=result['solverStarts'],
all_counted_model_evaluations=result['nfev'],
accepted_property_checks=s['accepted_property_checks']['calls'],
sample_count=len(result['series']['time']), jacobian_kernel_reuse=result['jacobianReuse'])
return dict(totalSeconds=total, exclusiveSeconds=times, counters=c)
def run(args, prepared):
rows = []
for variant in prepared['variants']:
case = args.output.resolve() / variant['name']
baseline = None
for index in range(-args.warmups, args.repeats):
label = f'warmup-{index + args.warmups}' if index < 0 else f'run-{index}'
for kind in ('control', 'profiled'):
dest = case / f'{kind}-{label}'
dest.mkdir()
command = [str(case / kind / variant['executable']), '--method', 'BDF', '--start', '0',
'--stop', str(args.stop), '--sample-step', '.01', '--rtol', '1e-8',
'--max-step', '1e30', '--timeout', '300', '--output', str(dest / 'result.json'),
'--sample-file', str(dest / 'states.bin'), '--output-block-file', str(dest / 'outputs.bin')]
env = dict(os.environ)
env.pop('NATIVE_SOLVER_PROFILE', None)
if kind == 'profiled':
env['NATIVE_SOLVER_PROFILE'] = str(dest / 'profile.json')
start = time.perf_counter()
with (dest / 'stderr.log').open('wb') as err:
process = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=err, env=env, timeout=330)
elapsed = time.perf_counter() - start
result = json.loads((dest / 'result.json').read_bytes())
assert process.returncode == 0 and result['success'], str(dest)
comparable = {k: v for k, v in result.items() if k not in ('solveSeconds', 'solveCpuSeconds')}
comparable.update(statesSha256=digest(dest / 'states.bin'), outputsSha256=digest(dest / 'outputs.bin'))
if baseline is None:
baseline = comparable
differences = [k for k in baseline.keys() | comparable.keys() if baseline.get(k) != comparable.get(k)]
assert not differences, (str(dest), differences)
row = dict(case=case.name, variant=kind, label=label, warmup=index < 0,
processWallSeconds=elapsed, solveSeconds=result['solveSeconds'],
solveCpuSeconds=result['solveCpuSeconds'], fullParity=True,
statesSha256=comparable['statesSha256'], outputsSha256=comparable['outputsSha256'])
if kind == 'profiled':
row['profile'] = analyze(json.loads((dest / 'profile.json').read_bytes()), result)
write(dest / 'measurement.json', row)
rows.append(row)
print(f'{case.name}/{kind}/{label}: solve {result["solveSeconds"]:.4f}s; parity OK', flush=True)
# Release the large decoded series before the next case.
del baseline
write(args.output / 'measurements.json', rows)
return summarize(args.output, prepared, rows)
def summarize(output, prepared, rows):
summary = dict(scope=prepared['scope'], cases=[])
for variant in prepared['variants']:
selected = [r for r in rows if r['case'] == variant['name'] and not r['warmup']]
profiled = [r for r in selected if r['variant'] == 'profiled']
control = [r for r in selected if r['variant'] == 'control']
c = profiled[0]['profile']['counters']
assert all(r['profile']['counters'] == c for r in profiled)
raw_profiles = [json.loads((output / variant['name'] / f'profiled-{r["label"]}' / 'profile.json').read_bytes())
for r in profiled]
times = {key: statistics.mean(r['profile']['exclusiveSeconds'][key] for r in profiled) for key in PARTITION}
total = statistics.mean(r['profile']['totalSeconds'] for r in profiled)
medians = {name: {key: statistics.median(r[key] for r in data)
for key in ('solveSeconds', 'solveCpuSeconds', 'processWallSeconds')}
for name, data in (('control', control), ('profiled', profiled))}
summary['cases'].append(dict(name=variant['name'], meanTotalSeconds=total,
phases={key: dict(seconds=value, percent=100 * value / total) for key, value in times.items()},
meanScopes={key: {metric: statistics.mean(r['scopes'][key][metric] for r in raw_profiles)
for metric in ('inclusiveSeconds', 'exclusiveSeconds')}
for key in raw_profiles[0]['scopes']},
symbolic_factorization=dict(seconds=0, status='not applicable: dense LU'),
error_estimation=dict(seconds=None, status='included in cvode_controller_including_error_estimation'),
counters=c, medians=medians,
observedOverheadPercent={key: 100 * (medians['profiled'][key] / medians['control'][key] - 1)
for key in medians['control']},
allRunsFullParity=True, samples=len(profiled)))
write(output / 'summary.json', summary)
print(json.dumps(summary, ensure_ascii=False, indent=2), flush=True)
return summary
def report_existing(output):
prepared = json.loads((output / 'prepared.json').read_bytes())
rows = json.loads((output / 'measurements.json').read_bytes())
for row in rows:
if row['variant'] != 'profiled':
continue
directory = output / row['case'] / f'profiled-{row["label"]}'
row['profile'] = analyze(json.loads((directory / 'profile.json').read_bytes()),
json.loads((directory / 'result.json').read_bytes()))
write(directory / 'measurement.json', row)
write(output / 'measurements.json', rows)
return summarize(output, prepared, rows)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--input', type=Path, default=ROOT / 'tests/data/test-mql-8-corrected.json')
parser.add_argument('--output', type=Path, default=ROOT / 'test/solver-profile-20260916')
parser.add_argument('--stop', type=float, default=21.7)
parser.add_argument('--repeats', type=int, default=3)
parser.add_argument('--warmups', type=int, default=1)
mode = parser.add_mutually_exclusive_group()
mode.add_argument('--run', action='store_true', help='also run after all builds complete')
mode.add_argument('--report-only', action='store_true', help='rebuild summary from completed runs; no build or simulation')
options = parser.parse_args()
if options.report_only:
report_existing(options.output)
else:
prepared = prepare(options)
if options.run:
run(options, prepared)
+62
View File
@@ -0,0 +1,62 @@
#include "solver_profile.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#ifdef _WIN32
#include <windows.h>
#endif
static struct { unsigned long long calls; double inclusive, exclusive; } totals[P_COUNT];
static unsigned long long counters[C_COUNT], errors;
static ProfileScope *parent;
static double profile_now(void) {
#ifdef _WIN32
static LARGE_INTEGER frequency;
LARGE_INTEGER value;
if(!frequency.QuadPart) QueryPerformanceFrequency(&frequency);
QueryPerformanceCounter(&value);
return (double)value.QuadPart/(double)frequency.QuadPart;
#else
struct timespec value; clock_gettime(CLOCK_MONOTONIC,&value);
return value.tv_sec+value.tv_nsec*1e-9;
#endif
}
ProfileScope profile_begin(int id) {
ProfileScope s={.id=id,.active=1,.parent=parent};
s.start=profile_now();return s;
}
void profile_link(ProfileScope *s) {parent=s;}
void profile_end(ProfileScope *s) {
if(!s->active)return;
double elapsed=profile_now()-s->start;
if(parent!=s || elapsed<s->children) {fputs("Invalid profiling scope\n",stderr);exit(74);}
totals[s->id].calls++;
totals[s->id].inclusive+=elapsed;
totals[s->id].exclusive+=elapsed-s->children;
parent=s->parent;
if(parent)parent->children+=elapsed;
s->active=0;
}
void profile_add(int id,int status,long int value) {
if(status || value<0)errors++;
else counters[id]+=(unsigned long long)value;
}
void profile_dump(void) {
const char *path=getenv("NATIVE_SOLVER_PROFILE");
if(!path || parent) {fputs("Missing profile destination or unclosed scope\n",stderr);exit(74);}
FILE *f=fopen(path,"wb");if(!f){perror(path);exit(73);}
const char *names[]={"total","integration","cvode_controller","rhs","nonlinear_residual",
"jacobian","jacobian_probe","linear_setup","numerical_factorization","linear_solve",
"newton","event_detection","accepted_property_checks","sampling","append","flush",
"output_replay","result_encoding","poll"};
const char *count_names[]={"accepted_steps","residual_evaluations","linear_rhs_evaluations",
"jacobian_evaluations","linear_setups","error_test_failures","newton_iterations",
"newton_failures","nonlinear_step_failures","counter_segments","jacobian_reuses",
"jacobian_refresh_setups","linear_setup_failures","LU_factorization_failures",
"LU_solve_failures","event_model_evaluations","scheduled_boundary_count"};
fprintf(f,"{\"version\":1,\"counterErrors\":%llu,\"counters\":{",errors);
for(int i=0;i<C_COUNT;i++)fprintf(f,"%s\"%s\":%llu",i?",":"",count_names[i],counters[i]);
fputs("},\"scopes\":{",f);
for(int i=0;i<P_COUNT;i++)fprintf(f,"%s\"%s\":{\"calls\":%llu,\"inclusiveSeconds\":%.17g,\"exclusiveSeconds\":%.17g}",
i?",":"",names[i],totals[i].calls,totals[i].inclusive,totals[i].exclusive);
fputs("}}\n",f);int ok=!ferror(f);if(fclose(f))ok=0;if(!ok)exit(73);
}
+27
View File
@@ -0,0 +1,27 @@
/* Diagnostic-only scopes for one standalone worker. Never linked in production. */
#ifndef SOLVER_PROFILE_H
#define SOLVER_PROFILE_H
enum {
P_TOTAL, P_INTEGRATION, P_CVODE, P_RHS, P_RESIDUAL, P_JACOBIAN,
P_JACOBIAN_PROBE, P_LSETUP, P_LU, P_LSOLVE, P_NEWTON, P_EVENT,
P_PROPERTY, P_SAMPLING, P_APPEND, P_FLUSH, P_REPLAY, P_ENCODING,
P_POLL, P_COUNT
};
enum {
C_STEPS, C_RHS, C_LINEAR_RHS, C_JACOBIANS, C_SETUPS, C_ERROR_FAILS,
C_NEWTON_ITERS, C_NEWTON_FAILS, C_STEP_SOLVE_FAILS, C_SEGMENTS,
C_JAC_REUSES, C_JAC_REFRESHES, C_SETUP_FAILURES, C_LU_FAILURES,
C_SOLVE_FAILURES, C_FRICTION_EVALS, C_BOUNDARIES, C_COUNT
};
typedef struct ProfileScope {
double start, children;
int id, active;
struct ProfileScope *parent;
} ProfileScope;
ProfileScope profile_begin(int id);
void profile_link(ProfileScope *scope);
void profile_end(ProfileScope *scope);
void profile_add(int id, int status, long int value);
void profile_dump(void);
#define PROFILE_SCOPE(id) ProfileScope profile_scope __attribute__((cleanup(profile_end)))=profile_begin(id); profile_link(&profile_scope)
#endif
+54
View File
@@ -0,0 +1,54 @@
/* Included only by the instrumented private copy of cvode_solver.c.
* Public SUN solver ops wrap the unchanged implementations from the same
* CVODE library. No private CVODE layouts or substituted numerical kernels. */
#include <sunnonlinsol/sunnonlinsol_newton.h>
static int (*original_lu)(SUNLinearSolver,SUNMatrix);
static int (*original_solve)(SUNLinearSolver,SUNMatrix,N_Vector,N_Vector,sunrealtype);
static int (*original_newton)(SUNNonlinearSolver,N_Vector,N_Vector,N_Vector,sunrealtype,sunbooleantype,void *);
static SUNErrCode (*original_setsys)(SUNNonlinearSolver,SUNNonlinSolSysFn);
static SUNErrCode (*original_setsetup)(SUNNonlinearSolver,SUNNonlinSolLSetupFn);
static SUNNonlinSolSysFn original_residual;
static SUNNonlinSolLSetupFn original_setup;
static int profiled_lu(SUNLinearSolver ls,SUNMatrix a) {
PROFILE_SCOPE(P_LU);
int flag=original_lu(ls,a);if(flag)profile_add(C_LU_FAILURES,0,1);return flag;
}
static int profiled_solve(SUNLinearSolver ls,SUNMatrix a,N_Vector x,N_Vector b,sunrealtype tol) {
PROFILE_SCOPE(P_LSOLVE);
int flag=original_solve(ls,a,x,b,tol);if(flag)profile_add(C_SOLVE_FAILURES,0,1);return flag;
}
static int profiled_residual(N_Vector y,N_Vector f,void *mem) {
PROFILE_SCOPE(P_RESIDUAL);return original_residual(y,f,mem);
}
static int profiled_setup(sunbooleantype bad,sunbooleantype *current,void *mem) {
PROFILE_SCOPE(P_LSETUP);
long int before=0,after=0;
int first=CVodeGetNumJacEvals(mem,&before);
int flag=original_setup(bad,current,mem);
int last=CVodeGetNumJacEvals(mem,&after);
/* Count successful linear setups which actually reused the stored J.
This is unrelated to scalar/kernel memoization inside a fresh J. */
if(flag)profile_add(C_SETUP_FAILURES,0,1);
else profile_add(after==before?C_JAC_REUSES:C_JAC_REFRESHES,first||last,1);
return flag;
}
static SUNErrCode profiled_setsys(SUNNonlinearSolver nls,SUNNonlinSolSysFn fn) {
original_residual=fn;return original_setsys(nls,fn?profiled_residual:NULL);
}
static SUNErrCode profiled_setsetup(SUNNonlinearSolver nls,SUNNonlinSolLSetupFn fn) {
original_setup=fn;return original_setsetup(nls,fn?profiled_setup:NULL);
}
static int profiled_newton(SUNNonlinearSolver nls,N_Vector y0,N_Vector y,N_Vector w,
sunrealtype tol,sunbooleantype setup,void *mem) {
PROFILE_SCOPE(P_NEWTON);return original_newton(nls,y0,y,w,tol,setup,mem);
}
static SUNNonlinearSolver profile_attach_newton(N_Vector y,SUNContext ctx) {
SUNNonlinearSolver nls=SUNNonlinSol_Newton(y,ctx);
if(!nls)return NULL;
original_newton=nls->ops->solve;original_setsys=nls->ops->setsysfn;original_setsetup=nls->ops->setlsetupfn;
nls->ops->solve=profiled_newton;nls->ops->setsysfn=profiled_setsys;nls->ops->setlsetupfn=profiled_setsetup;
return nls;
}
static int profiled_cvode(void *solver,sunrealtype end,N_Vector y,sunrealtype *next,int task) {
PROFILE_SCOPE(P_CVODE);return CVode(solver,end,y,next,task);
}
+231
View File
@@ -0,0 +1,231 @@
"""Jacobian-local reuse: complete keys, isolation and uncached C parity."""
from pathlib import Path
import subprocess
import tempfile
import unittest
from app.simulation.native_codegen.build import toolchain, _command, COMPILER_FLAGS
from app.simulation.native_codegen.extended import compile_extended_program
from tests.test_native_catalog import Circuit
from tests.native_reference import reference_data, reference_network
ROOT=Path(__file__).resolve().parents[1]
def c_check(source,program=None):
with tempfile.TemporaryDirectory() as temporary:
directory=Path(temporary)
if program:
(directory/'model.h').write_text(program.header,encoding='utf-8')
(directory/'model.c').write_text(program.source,encoding='utf-8')
(directory/'test.c').write_text(source,encoding='utf-8')
exe=directory/('test.exe' if __import__('os').name=='nt' else 'test')
_command([toolchain()[0],*COMPILER_FLAGS,'-I',str(ROOT/'native/include'),'-I',str(directory),
str(directory/'test.c'),str(ROOT/'native/components/kernels.c'),'-lm','-o',str(exe)],log=[])
result=subprocess.run([str(exe)],capture_output=True,text=True,timeout=60)
if result.returncode: raise AssertionError(result.stdout+result.stderr)
class JacobianReuseTests(unittest.TestCase):
def test_scalar_collision_capacity_keys_and_recording(self):
c_check(r'''
#include "kernels.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
#define CHECK(x) do {if(!(x)){fprintf(stderr,"line %d\n",__LINE__);return 1;}}while(0)
int main(void) {
NativeJacobianScalarEntry entries[16]={0};
NativeJacobianScalars cache={.entries=entries,.capacity=1,.recording=1};
double key[10]={1,2,3,4,5,6,7,8,9,10},changed[10],value;
native_jacobian_scalar_put(&cache,NATIVE_JACOBIAN_PH,1,key,10,11);
CHECK(native_jacobian_scalar_get(&cache,NATIVE_JACOBIAN_PH,1,key,10,&value) && value==11);
/* A one-slot cache guarantees collisions and saturation. */
for(int i=0;i<10;i++) {
memcpy(changed,key,sizeof(key));changed[i]=nextafter(changed[i],INFINITY);
CHECK(!native_jacobian_scalar_get(&cache,NATIVE_JACOBIAN_PH,1,changed,10,&value));
native_jacobian_scalar_put(&cache,NATIVE_JACOBIAN_PH,1,changed,10,12);
CHECK(native_jacobian_scalar_get(&cache,NATIVE_JACOBIAN_PH,1,key,10,&value) && value==11);
}
CHECK(!native_jacobian_scalar_get(&cache,NATIVE_JACOBIAN_DENSITY,1,key,10,&value));
CHECK(!native_jacobian_scalar_get(&cache,NATIVE_JACOBIAN_PH,0,key,10,&value));
CHECK(!native_jacobian_scalar_get(&cache,NATIVE_JACOBIAN_PH,1,key,9,&value));
memset(entries,0,sizeof(entries));cache.capacity=16;
key[0]=0;native_jacobian_scalar_put(&cache,NATIVE_JACOBIAN_PH,1,key,10,1);
key[0]=-0.;CHECK(!native_jacobian_scalar_get(&cache,NATIVE_JACOBIAN_PH,1,key,10,&value));
native_jacobian_scalar_put(&cache,NATIVE_JACOBIAN_PH,1,key,10,NAN);
CHECK(!native_jacobian_scalar_get(&cache,NATIVE_JACOBIAN_PH,1,key,10,&value));
cache.recording=0;native_jacobian_scalar_put(&cache,NATIVE_JACOBIAN_PH,1,key,10,2);
CHECK(!native_jacobian_scalar_get(&cache,NATIVE_JACOBIAN_PH,1,key,10,&value));
key[0]=0;CHECK(native_jacobian_scalar_get(&cache,NATIVE_JACOBIAN_PH,1,key,10,&value) && value==1);
cache.capacity=0;CHECK(!native_jacobian_scalar_get(&cache,NATIVE_JACOBIAN_PH,1,key,10,&value));
cache.capacity=3;CHECK(!native_jacobian_scalar_get(&cache,NATIVE_JACOBIAN_PH,1,key,10,&value));
return 0;
}
''')
def test_scalar_reuse_replays_property_effects_even_with_full_local_cache(self):
c_check(r'''
#include "kernels.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
#define CHECK(x) do {if(!(x)){fprintf(stderr,"line %d\n",__LINE__);return 1;}}while(0)
int main(void) {
NativeMedium m={1,2077.26439404998,5193.16098512495,293.15,0,1.96e-5,293.15,79.4};
NativeJacobianScalarEntry entries[64]={0};
NativeJacobianScalars memo={.entries=entries,.capacity=64,.recording=1};
double initial[2];NativeGas gas;
CHECK(native_medium_init(&m,2e5,300,.01,0,initial));
CHECK(native_medium_gas_context(NULL,&m,initial[0],initial[1],.01,&gas));
for(int iteration=0;iteration<4;iteration++) {
NativePropertyState a[32],b[32];NativePropertyCache ca,cb;
NativePropertyTemperatures ta={0},tb={0};
native_properties_init(&ca,a,iteration%2?0:32);native_properties_init(&cb,b,iteration%2?0:32);
ca.temperatures=&ta;cb.temperatures=&tb;cb.jacobian=&memo;
double t1=native_temperature_ph_context(&ca,&m,2e5,gas.h),t2=native_temperature_ph_context(&cb,&m,2e5,gas.h);
CHECK(!memcmp(&t1,&t2,sizeof(t1)));
double r1=native_density_context(&ca,&m,2e5,t1),r2=native_density_context(&cb,&m,2e5,t2);
CHECK(!memcmp(&r1,&r2,sizeof(r1)));
double q1=native_pipe_flow_context(&ca,&m,2e5,1e5,t1,.001,1,.001,1);
double q2=native_pipe_flow_context(&cb,&m,2e5,1e5,t2,.001,1,.001,1);
CHECK(isfinite(q1) && !memcmp(&q1,&q2,sizeof(q1)));
CHECK(ca.count==cb.count && ta.seen==tb.seen);
for(int i=0;i<3;i++)CHECK(ta.minimum[i]==tb.minimum[i] && ta.maximum[i]==tb.maximum[i]);
for(size_t i=0;i<ca.count;i++) {
/* Compare every numerical field and validity flag; owner pointers differ. */
a[i].temperatures=b[i].temperatures=NULL;a[i].jacobian=b[i].jacobian=NULL;
CHECK(!memcmp(&a[i],&b[i],sizeof(a[i])));
}
memo.recording=0;
}
for(int i=0;i<NATIVE_JACOBIAN_SCALAR_KINDS;i++)CHECK(memo.reuses[i]>0);
return 0;
}
''')
def test_complete_bit_keys_immutable_baseline_failure_and_isolation(self):
c_check(r'''
#include "kernels.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
#define CHECK(x) do {if(!(x)){fprintf(stderr,"line %d\n",__LINE__);return 1;}}while(0)
static int calls;
static int kernel(NativePropertyCache *cache,const NativeMedium *m,double mass,double thermal,double volume,NativeGas *g) {
if(cache)return 0;
calls++;
if(!isfinite(thermal))return 0;
*g=(NativeGas){mass,thermal,volume,m->R,m->cp};return 1;
}
static int alternate(NativePropertyCache *c,const NativeMedium *m,double a,double b,double v,NativeGas *g) {
return kernel(c,m,a,b,v,g);
}
int main(void) {
NativeMedium m={1,2077,5193,293,0,1.96e-5,293,79.4};
NativeJacobianGasMemo memo={0},other={0};NativeJacobianGasStats stats={0};NativeGas g,base;
CHECK(native_jacobian_gas(&memo,kernel,&m,1,0,.01,&base,&stats));CHECK(calls==1);
NativeMedium copy=m;
CHECK(native_jacobian_gas(&memo,kernel,&copy,1,0,.01,&g,&stats));
CHECK(calls==1 && stats.reuses==1 && !memcmp(&g,&base,sizeof(g)));
double *fields[]={&copy.R,&copy.cp,&copy.Tref,&copy.slope,&copy.mu,&copy.muT,&copy.S};
for(int i=0;i<7;i++) {
copy=m;*fields[i]=nextafter(*fields[i],INFINITY);
int before=calls;CHECK(native_jacobian_gas(&memo,kernel,&copy,1,0,.01,&g,&stats));CHECK(calls==before+1);
}
copy=m;copy.real_helium=0;
int before=calls;CHECK(native_jacobian_gas(&memo,kernel,&copy,1,0,.01,&g,&stats));CHECK(calls==before+1);
before=calls;CHECK(native_jacobian_gas(&memo,alternate,&m,1,0,.01,&g,&stats));CHECK(calls==before+1);
before=calls;CHECK(native_jacobian_gas(&memo,kernel,&m,nextafter(1,2),0,.01,&g,&stats));CHECK(calls==before+1);
before=calls;CHECK(native_jacobian_gas(&memo,kernel,&m,1,nextafter(0,1),.01,&g,&stats));CHECK(calls==before+1);
before=calls;CHECK(native_jacobian_gas(&memo,kernel,&m,1,0,nextafter(.01,1),&g,&stats));CHECK(calls==before+1);
before=calls;CHECK(native_jacobian_gas(&memo,kernel,&m,1,-0.,.01,&g,&stats));CHECK(calls==before+1 && signbit(g.T));
before=calls;CHECK(!native_jacobian_gas(&memo,kernel,&m,1,NAN,.01,&g,&stats));CHECK(calls==before+1);
/* Misses must not evict the original baseline; another workspace starts empty. */
before=calls;CHECK(native_jacobian_gas(&memo,kernel,&m,1,0,.01,&g,&stats));CHECK(calls==before);
CHECK(!memcmp(&g,&base,sizeof(g)));
CHECK(!native_jacobian_gas(&other,kernel,&m,1,INFINITY,.01,&g,&stats));CHECK(!other.valid);
CHECK(native_jacobian_gas(&other,kernel,&m,1,0,.01,&g,&stats));CHECK(other.valid);
memset(&memo,0,sizeof(memo));before=calls;
CHECK(native_jacobian_gas(&memo,kernel,&m,1,0,.01,&g,&stats));CHECK(calls==before+1);
CHECK(stats.evaluations==(unsigned long)calls);
return 0;
}
''')
def check_program(self,program):
c_check(r'''
#include "model.c"
#include <stdio.h>
#include <string.h>
#define CHECK(x) do {if(!(x)){fprintf(stderr,"line %d iteration %d\n",__LINE__,iteration);return 1;}}while(0)
int main(void) {
int iteration=-1;double original[NSTATES],baseline[NSTATES],wo[NOUTPUTS];
ModelJacobianWorkspace workspace={0};
model_jacobian_begin(&workspace);
CHECK(model_init(original));
CHECK(model_eval_jacobian_reuse(.003,original,baseline,wo,&workspace));
CHECK(workspace.stats.evaluations==MODEL_JACOBIAN_GAS_COUNT);
for(iteration=0;iteration<2*NSTATES+4;iteration++) {
double y[NSTATES],a[NSTATES],b[NSTATES],w[NOUTPUTS],v[NOUTPUTS];
memcpy(y,original,sizeof(y));
if(iteration<2*NSTATES)y[iteration/2]+=(iteration%2?1:-1)*1e-8*fmax(1,fabs(y[iteration/2]));
if(iteration==2*NSTATES+1)y[0]=NAN;
if(iteration==2*NSTATES+2)y[0]=INFINITY;
int ok=model_eval_jacobian(.003,y,a,w);
int cached=model_eval_jacobian_reuse(.003,y,b,v,&workspace);
CHECK(ok==cached);
if(ok)CHECK(!memcmp(a,b,sizeof(a)) && !memcmp(w,v,sizeof(w)));
}
CHECK(workspace.stats.reuses>0);
/* Baseline remains usable after all perturbations and invalid trials. */
unsigned long before=workspace.stats.evaluations;
CHECK(model_eval_jacobian_reuse(.003,original,baseline,wo,&workspace));
CHECK(workspace.stats.evaluations==before);
return 0;
}
''',program)
def test_thermal_storage_reverse_flow_and_moving_volume(self):
b=Circuit();left=b.chamber('arbitrary_left',p0=8e4,T0=100,kth=0)
pipe=b.add('amesim_pnl0002','arbitrary_pipe',p0=1e5,T0=1700,kth=0,diam=.001,le=1)
right=b.chamber('arbitrary_right',p0=2e5,T0=300,kth=0)
b.connect(left,'port_1',pipe,'port_1');b.connect(pipe,'port_2',right,'port_1')
self.check_program(compile_extended_program(b.seal()))
# Use catalog equations, including mechanical volume inputs, without
# encoding any benchmark component names into the reuse implementation.
case=next(c for c in reference_data()['cases'] if any(x['type']=='amesim_pnch012' for x in c['components']))
self.check_program(compile_extended_program(reference_network(case)))
def test_polytropic_and_ideal_medium_kernels(self):
c_check(r'''
#include "kernels.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
#define CHECK(x) do {if(!(x)){fprintf(stderr,"line %d\n",__LINE__);return 1;}}while(0)
int main(void) {
NativeMedium m={0,287,1005,293,.01,1.8e-5,293,110.4};
for(int helium=0;helium<2;helium++)for(int poly=0;poly<2;poly++) {
m.real_helium=helium;if(helium){m.R=2077.26439404998;m.cp=5193.16098512495;}
NativeCanonicalGas kernel=poly?native_polytropic_gas_context:native_medium_gas_context;
NativeJacobianGasMemo memo={0};NativeJacobianGasStats stats={0};
double temperatures[]={100,300,1700,25000,300};
for(int i=0;i<5;i++) {
double state[2];NativeGas a,b;
CHECK(native_medium_init(&m,1e5,temperatures[i],.01,0,state));
double thermal=poly?temperatures[i]:state[1];
int ok=kernel(NULL,&m,state[0],thermal,.01,&a);
int reused=native_jacobian_gas(&memo,kernel,&m,state[0],thermal,.01,&b,&stats);
CHECK(ok==reused);if(ok)CHECK(!memcmp(&a,&b,sizeof(a)));
reused=native_jacobian_gas(&memo,kernel,&m,state[0],thermal,.01,&b,&stats);
CHECK(ok==reused);if(ok)CHECK(!memcmp(&a,&b,sizeof(a)));
}
CHECK(stats.reuses>0);
}
return 0;
}
''')
if __name__=='__main__': unittest.main()
+158
View File
@@ -0,0 +1,158 @@
"""Exercise production reuse dispatch with public SUNDIALS types on either OS.
Only CVODE's weight/step getters are controlled; the production Jacobian,
colored/dense recovery and verification logic are compiled unchanged.
"""
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
from app.simulation.native_codegen.build import toolchain, platform_build_inputs, link_library_arguments, _command
ROOT=Path(__file__).resolve().parents[1]
HEADER=r'''
#ifndef TEST_MODEL_H
#define TEST_MODEL_H
#include "kernels.h"
#define NSTATES 4
#define NOUTPUTS 4
#define MODEL_JACOBIAN_COLORED 1
#define MODEL_JACOBIAN_CANONICAL_RHS 1
#define MODEL_JACOBIAN_COLOR_COUNT 2
#define MODEL_JACOBIAN_NNZ 8
#define MODEL_JACOBIAN_GAS_REUSE 1
typedef struct {NativeJacobianGasStats stats;NativeJacobianScalars scalars;double baseline[4];int valid;} ModelJacobianWorkspace;
void model_jacobian_begin(ModelJacobianWorkspace *);
int model_eval_jacobian_reuse(double,const double *,double *,double *,ModelJacobianWorkspace *);
extern const int model_jacobian_column_color[4],model_jacobian_col_ptr[5],model_jacobian_row_index[8];
extern const double model_atol[4];
double model_next_break(double,double);
#endif
'''
HARNESS=r'''
#include <cvode/cvode.h>
#include <nvector/nvector_serial.h>
#include <sunmatrix/sunmatrix_dense.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int test_weights(void *solver,N_Vector weights){(void)solver;N_VConst(1e6,weights);return 0;}
static int test_step(void *solver,sunrealtype *step){(void)solver;*step=.01;return 0;}
#define CVodeGetErrWeights test_weights
#define CVodeGetCurrentStep test_step
#include "@RUNTIME@"
#define CHECK(x) do {if(!(x)){fprintf(stderr,"line %d\n",__LINE__);exit(1);}}while(0)
const int model_jacobian_column_color[4]={0,1,0,1},model_jacobian_col_ptr[5]={0,2,4,6,8};
const int model_jacobian_row_index[8]={0,1,0,1,2,3,2,3};
const double model_atol[4]={1e-9,1e-9,1e-9,1e-9};
static int cached_calls,uncached_calls,begins,reject_group,reject_individual,reject_base,corrupt;
static int polls,cancel_at;
static double base_state[4]={1,2,3,4};
static int changed(const double *y){int n=0;for(int i=0;i<4;i++)n+=y[i]!=base_state[i];return n;}
static void evaluate(const double *y,double *f) {
for(int i=0;i<4;i+=2){f[i]=y[i]*y[i]+.5*y[i+1];f[i+1]=sin(y[i+1])+.25*y[i];}
}
int native_poll(NativeRun *r,double t){(void)r;(void)t;polls++;return !cancel_at || polls<cancel_at;}
int native_rhs(NativeRun *r,double t,const double *y,double *f){(void)t;r->nfev++;evaluate(y,f);return 1;}
int native_jacobian_rhs(NativeRun *r,double t,const double *y,double *f) {
(void)t;r->nfev++;uncached_calls++;if(reject_individual && changed(y))return 0;evaluate(y,f);return 1;
}
void model_jacobian_begin(ModelJacobianWorkspace *w){memset(w,0,sizeof(*w));begins++;}
int model_eval_jacobian_reuse(double t,const double *y,double *f,double *outputs,ModelJacobianWorkspace *w) {
(void)t;(void)outputs;cached_calls++;w->stats.evaluations++;w->scalars.evaluations[0]+=2;
int n=changed(y);if((reject_base && !n) || (reject_group && n>1) || (reject_individual && n))return 0;
if(!w->valid){memcpy(w->baseline,y,sizeof(w->baseline));w->valid=1;}
evaluate(y,f);if(corrupt && n)f[0]+=1;return 1;
}
int native_fail(NativeRun *r,const char *a,const char *b,const char *c){(void)a;(void)b;(void)c;r->status=2;return 0;}
double model_next_break(double t,double end){(void)t;return end;}
int native_accept(NativeRun *r,double t,double next,const double *old,const double *trial,NativeDense dense,
void *context,double *at,double *accepted){(void)r;(void)t;(void)next;(void)old;(void)trial;(void)dense;(void)context;(void)at;(void)accepted;return -1;}
typedef struct {SUNContext sun;NativeRun run;CvContext context;N_Vector y,fy,tmp1,tmp2,tmp3;SUNMatrix matrix;} Fixture;
static void state(Fixture *f) {
memcpy(N_VGetArrayPointer(f->y),base_state,sizeof(base_state));
evaluate(base_state,N_VGetArrayPointer(f->fy));
for(int i=0;i<4;i++)N_VGetArrayPointer(f->fy)[i]+=.125; /* ordinary fy determines increments */
}
static void init(Fixture *f) {
memset(f,0,sizeof(*f));CHECK(!SUNContext_Create(SUN_COMM_NULL,&f->sun));
f->y=N_VNew_Serial(4,f->sun);f->fy=N_VClone(f->y);f->tmp1=N_VClone(f->y);f->tmp2=N_VClone(f->y);f->tmp3=N_VClone(f->y);
f->context=(CvContext){.run=&f->run,.colored=1,.weights=N_VClone(f->y)};f->run.jacobian_colored=1;
f->context.jacobian_workspace=malloc(sizeof(*f->context.jacobian_workspace));CHECK(f->context.jacobian_workspace);
f->matrix=SUNDenseMatrix(4,4,f->sun);state(f);
}
static int callback(Fixture *f){return cv_jacobian(.5,f->y,f->fy,f->matrix,&f->context,f->tmp1,f->tmp2,f->tmp3);}
static void matrix_correct(Fixture *f) {
double increments[4],base[4],trial[4],value[4],fy[4];CHECK(!jac_increments(&f->context,f->y,f->fy,increments));
evaluate(base_state,base);
for(int j=0;j<4;j++) {
memcpy(trial,base_state,sizeof(trial));trial[j]+=increments[j];evaluate(trial,value);
double inverse=1.0/increments[j];
for(int i=0;i<4;i++)CHECK(SM_ELEMENT_D(f->matrix,i,j)==inverse*(value[i]-base[i]));
}
CHECK(!memcmp(base_state,N_VGetArrayPointer(f->y),sizeof(base_state)));
evaluate(base_state,fy);for(int i=0;i<4;i++)fy[i]+=.125;
CHECK(!memcmp(fy,N_VGetArrayPointer(f->fy),sizeof(fy)));
}
int main(void) {
Fixture f;init(&f);CHECK(valid_coloring());
CHECK(!callback(&f));CHECK(cached_calls==3 && uncached_calls==0 && begins==1);matrix_correct(&f);
CHECK(f.run.nfev==3 && f.run.jacobian_rhs==3 && f.run.jacobian_gas_evaluations==3 && f.run.jacobian_scalar_evaluations[0]==6);
for(int i=0;i<4;i++){base_state[i]+=.125;}state(&f);
CHECK(!callback(&f));CHECK(begins==2 && !memcmp(f.context.jacobian_workspace->baseline,base_state,sizeof(base_state)));matrix_correct(&f);
/* Optional allocation failure retains ordinary canonical coloring. */
ModelJacobianWorkspace *saved=f.context.jacobian_workspace;f.context.jacobian_workspace=NULL;
int prior=cached_calls,prior_uncached=uncached_calls;
CHECK(!callback(&f));CHECK(cached_calls==prior && uncached_calls-prior_uncached==3);matrix_correct(&f);
f.context.jacobian_workspace=saved;
int cached=cached_calls,uncached=uncached_calls;unsigned long before=f.run.nfev;
reject_group=1;CHECK(!callback(&f));CHECK(cached_calls-cached==2 && uncached_calls-uncached==4 && f.run.nfev-before==6);
CHECK(f.run.jacobian_fallbacks==1 && f.context.colored);matrix_correct(&f);reject_group=0;
cached=cached_calls;uncached=uncached_calls;reject_individual=1;
CHECK(callback(&f)>0);CHECK(cached_calls-cached==2 && uncached_calls-uncached==1);reject_individual=0;
CHECK(f.run.jacobian_fallbacks==2);
cached=cached_calls;uncached=uncached_calls;reject_base=1;
CHECK(callback(&f)>0);CHECK(cached_calls-cached==1 && uncached_calls==uncached);reject_base=0;
CHECK(f.run.jacobian_fallbacks==2);
for(int first=1;first<=2;first++) {
cached=cached_calls;uncached=uncached_calls;before=f.run.nfev;polls=0;cancel_at=first;
CHECK(callback(&f)<0);CHECK(cached_calls-cached==first-1 && uncached_calls==uncached && f.run.nfev-before==(unsigned)(first-1));
CHECK(f.run.jacobian_fallbacks==2);cancel_at=0;
}
f.context.reference=SUNDenseMatrix(4,4,f.sun);corrupt=1;cached=cached_calls;uncached=uncached_calls;
CHECK(!callback(&f));CHECK(cached_calls-cached==3 && uncached_calls-uncached==4);
CHECK(f.run.jacobian_checks==1 && f.run.jacobian_mismatches==1 && f.run.jacobian_fallbacks==3);
CHECK(!f.context.colored && !f.run.jacobian_colored);matrix_correct(&f);
cached=cached_calls;uncached=uncached_calls;int resets=begins;
CHECK(!callback(&f));CHECK(cached_calls==cached && uncached_calls-uncached==5 && begins==resets);matrix_correct(&f);
CHECK(f.run.nfev==f.run.jacobian_rhs);
SUNMatDestroy(f.context.reference);SUNMatDestroy(f.matrix);
free(f.context.jacobian_workspace);
N_VDestroy(f.context.weights);N_VDestroy(f.y);N_VDestroy(f.fy);N_VDestroy(f.tmp1);N_VDestroy(f.tmp2);N_VDestroy(f.tmp3);SUNContext_Free(&f.sun);
puts("reuse runtime cases passed");return 0;
}
'''
class JacobianReuseRuntimeTests(unittest.TestCase):
def test_reset_reference_fallback_failure_and_cancellation(self):
compiler,sundials,_=toolchain()
flags,libraries,dlls,executable=platform_build_inputs(sundials)
with tempfile.TemporaryDirectory() as directory:
directory=Path(directory)
(directory/'model.h').write_text(HEADER,encoding='utf-8')
source=HARNESS.replace('@RUNTIME@',(ROOT/'native/runtime/cvode_solver.c').as_posix())
(directory/'test.c').write_text(source,encoding='utf-8')
target=directory/executable
_command([compiler,*flags,'-I',str(directory),'-I',str(ROOT/'native/include'),'-I',str(sundials/'include'),
str(directory/'test.c'),*link_library_arguments(libraries),'-lm','-o',str(target)],log=[])
for dll in dlls: shutil.copy2(dll,directory/dll.name)
result=subprocess.run([str(target)],capture_output=True,text=True,timeout=30)
self.assertEqual(result.returncode,0,result.stdout+result.stderr)
self.assertIn('reuse runtime cases passed',result.stdout)
if __name__=='__main__': unittest.main()
+65
View File
@@ -0,0 +1,65 @@
"""Check diagnostic scope accounting, including nested returns and goto cleanup."""
import json
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from app.simulation.native_codegen.build import toolchain, platform_build_inputs, _command
from tests.manual.profile_native_solver import instrument, PARTITION
ROOT = Path(__file__).resolve().parents[1]
HERE = ROOT / 'tests/manual'
HARNESS = r'''
#include "solver_profile.h"
static int early(void) { PROFILE_SCOPE(P_RHS);return 7; }
static void nested(void) {
PROFILE_SCOPE(P_INTEGRATION);
{ PROFILE_SCOPE(P_EVENT);if(early()==7)goto finished; }
finished:
profile_add(C_STEPS,0,3);
profile_add(C_ERROR_FAILS,-1,1);
}
int main(void) {
PROFILE_SCOPE(P_TOTAL);nested();profile_end(&profile_scope);profile_dump();return 0;
}
'''
class SolverProfileTests(unittest.TestCase):
def test_nested_early_return_and_goto_partition(self):
compiler, sundials, _ = toolchain()
flags, _, _, executable = platform_build_inputs(sundials)
with tempfile.TemporaryDirectory() as name:
directory = Path(name)
(directory / 'harness.c').write_text(HARNESS, encoding='utf-8')
_command([compiler, *flags, '-I', str(HERE), str(directory / 'harness.c'),
str(HERE / 'solver_profile.c'), '-o', str(directory / executable)], log=[])
environment = dict(os.environ, NATIVE_SOLVER_PROFILE=str(directory / 'profile.json'))
subprocess.run([str(directory / executable)], env=environment, check=True, timeout=20)
result = json.loads((directory / 'profile.json').read_bytes())
scopes = result['scopes']
self.assertEqual(result['counterErrors'], 1)
self.assertEqual(result['counters']['accepted_steps'], 3)
self.assertEqual(result['counters']['error_test_failures'], 0)
for key in ('total', 'integration', 'event_detection', 'rhs'):
self.assertEqual(scopes[key]['calls'], 1)
self.assertEqual(set(scopes), {key for group in PARTITION.values() for key in group})
self.assertEqual(len(scopes), sum(map(len, PARTITION.values())))
self.assertAlmostEqual(sum(s['exclusiveSeconds'] for s in scopes.values()),
scopes['total']['inclusiveSeconds'], places=9)
self.assertTrue(all(s['exclusiveSeconds'] >= 0 for s in scopes.values()))
def test_instrumentation_rejects_missing_anchors(self):
with tempfile.TemporaryDirectory() as name:
directory = Path(name)
(directory / 'include').mkdir()
(directory / 'runtime').mkdir()
(directory / 'runtime/common.c').write_text('/* incompatible runtime */')
with self.assertRaisesRegex(RuntimeError, 'Cannot identify unique function'):
instrument(directory)
if __name__ == '__main__':
unittest.main()