优化 Jacobian 确定性复用并补充性能剖析与平台依赖文档
在单次 Jacobian 构建内按完整输入精确复用储气物性、PH 反算、密度和管路求根结果,保持原有求值副作用、差分政策与失败回退。八路模型求解 CPU 中位数减少 19.27%,循环和不循环的完整原始采样均与恢复基线一致。 增加独立的跨平台时间剖析工具,记录互斥阶段耗时、Newton/LU 统计、矩阵复用与内核复用,保存 UD00 两种工况的调查报告和机器可读汇总。 补充 Windows/Linux 运行、测试、原生编译和剖析所需依赖文档及索引,不修改依赖清单、版本锁或安装环境。 验证:8 项新增专项回归通过;2270 次完整 Jacobian 核对零差异;16 次剖析配对及预热运行保持完整数值一致。既有固定样本哈希失败和 Linux 实机验收限制见报告。
This commit is contained in:
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()
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,©,1,0,.01,&g,&stats));
|
||||
CHECK(calls==1 && stats.reuses==1 && !memcmp(&g,&base,sizeof(g)));
|
||||
double *fields[]={©.R,©.cp,©.Tref,©.slope,©.mu,©.muT,©.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,©,1,0,.01,&g,&stats));CHECK(calls==before+1);
|
||||
}
|
||||
copy=m;copy.real_helium=0;
|
||||
int before=calls;CHECK(native_jacobian_gas(&memo,kernel,©,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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user