前端进度条性能优化、仿真结束后后处理优化;后端C代码生成流程优化:先识别来源,再按照已知未知量需求排序,最后局部求解
This commit is contained in:
1 parent
0dcb465d84
commit
91bd9fb252
77 files changed
+11223
-551
No files matched your search
@@ -0,0 +1,183 @@
|
||||
"""Dependency correctness and numerical isolation of generated local solves."""
|
||||
from copy import deepcopy
|
||||
from dataclasses import replace
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
from app.simulation.native_codegen.build import build_native, toolchain
|
||||
from app.simulation.native_codegen.compiler import NativeCapabilityError
|
||||
from app.simulation.native_codegen.extended import compile_extended_program
|
||||
from app.simulation.native_codegen.schedule import Computation as Op, EvaluationSchedule
|
||||
from tests.native_reference import reference_data, reference_network
|
||||
|
||||
|
||||
class DependencyGraphTests(unittest.TestCase):
|
||||
def test_deep_reverse_emission_order_and_state_origin(self):
|
||||
ops = [Op.assignment(str(i), f'h[{i}]', f'h[{i-1}]', 'alias') for i in range(2000, 0, -1)]
|
||||
plan = EvaluationSchedule(ops, {'h[0]': 'state:chamber.m,U'})
|
||||
self.assertFalse(any(block.cyclic for block in plan.blocks))
|
||||
self.assertEqual([plan.computations[b.members[0]].key for b in plan.blocks], list(map(str, range(1, 2001))))
|
||||
self.assertEqual(plan.report()['blocks'][-1]['origins'], ['state:chamber.m,U'])
|
||||
|
||||
def test_separate_cycles_and_dependent_output(self):
|
||||
ops = [Op.assignment('result', 'q[0]', 'h[1]+h[3]'),
|
||||
Op.assignment('a', 'h[1]', 'h[2]+g[0].h', 'stream'),
|
||||
Op.assignment('b', 'h[2]', 'h[1]', 'alias'),
|
||||
Op.assignment('c', 'h[3]', 'h[4]+g[1].h', 'stream'),
|
||||
Op.assignment('d', 'h[4]', 'h[3]', 'alias')]
|
||||
plan = EvaluationSchedule(ops, {'g[0].h': 'state:left', 'g[1].h': 'state:right'})
|
||||
self.assertEqual([b.members for b in plan.blocks if b.cyclic], [(1, 2), (3, 4)])
|
||||
self.assertEqual(plan.blocks[-1].members, (0,))
|
||||
self.assertEqual(plan.report()['blocks'][-1]['origins'], ['state:left', 'state:right'])
|
||||
|
||||
def test_missing_source_duplicate_producer_and_source_free_alias_cycle(self):
|
||||
with self.assertRaisesRegex(NativeCapabilityError, 'missing native input'):
|
||||
EvaluationSchedule([Op.assignment('a', 'h[1]', 'h[0]')], {'h[1]': 'initial guess'})
|
||||
with self.assertRaisesRegex(NativeCapabilityError, 'Multiple native producers'):
|
||||
EvaluationSchedule([Op.assignment('a', 'q[0]', '0'), Op.assignment('b', 'q[0]', '1')], {})
|
||||
plan = EvaluationSchedule([Op.assignment('a', 'h[0]', 'h[1]', 'alias'),
|
||||
Op.assignment('b', 'h[1]', 'h[0]', 'alias')],
|
||||
{'h[0]': 'initial guess', 'h[1]': 'initial guess'})
|
||||
with self.assertRaisesRegex(NativeCapabilityError, 'no thermodynamic source'):
|
||||
plan.emit()
|
||||
|
||||
|
||||
def prefix_case(case, prefix):
|
||||
result = deepcopy(case)
|
||||
for component in result['components']:
|
||||
component['name'] = prefix + component['name']
|
||||
result['connections'] = [[[prefix+name, port] for name, port in edge] for edge in result['connections']]
|
||||
return result
|
||||
|
||||
|
||||
def instrument(program):
|
||||
"""Count actual emitted operation executions; no timing counters in production."""
|
||||
count = program.evaluation_schedule['operationCount']
|
||||
code = re.sub(r'/\* schedule operation (\d+): \w+ \*/', r'++schedule_counts[\1];', program.source)
|
||||
code = code.replace('int model_eval(', 'static int model_eval_impl(', 1)
|
||||
code = '#include <stdio.h>\nstatic unsigned long long schedule_counts['+str(count)+'];\n'+code
|
||||
code += '''
|
||||
int model_eval(double t,const double *y,double *dy,double *w) {
|
||||
for(int i=0;i<'''+str(count)+''';i++) schedule_counts[i]=0;
|
||||
int ok=model_eval_impl(t,y,dy,w);
|
||||
fprintf(stderr,"[");
|
||||
for(int i=0;i<'''+str(count)+''';i++) fprintf(stderr,"%s%llu",i?",":"",schedule_counts[i]);
|
||||
fprintf(stderr,"]\\n");return ok;
|
||||
}
|
||||
'''
|
||||
return replace(program, source=code)
|
||||
|
||||
|
||||
class NativeScheduleTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
try:
|
||||
toolchain()
|
||||
except (OSError, RuntimeError, subprocess.SubprocessError) as exc:
|
||||
raise unittest.SkipTest(f'Native toolchain unavailable: {exc}')
|
||||
|
||||
def probe(self, program, states):
|
||||
build = build_native(instrument(program))
|
||||
inputs = ''.join(' '.join(format(v, '.17g') for v in (0, *[state[k] for k in program.state_keys]))+'\n' for state in states)
|
||||
run = subprocess.run([str(build.executable), '--probe'], input=inputs, capture_output=True, text=True, check=True, timeout=30)
|
||||
rows = [json.loads(line) for line in run.stdout.splitlines()]
|
||||
counts = [json.loads(line) for line in run.stderr.splitlines()]
|
||||
self.assertEqual(len(rows), len(states))
|
||||
self.assertEqual(len(counts), len(states))
|
||||
for row in rows:
|
||||
self.assertTrue(row['success'])
|
||||
return rows, counts
|
||||
|
||||
def test_two_pressure_loops_do_not_recompute_each_other_or_direct_branch(self):
|
||||
data = reference_data()['cases']
|
||||
a, b, direct = prefix_case(data[37], 'A_'), prefix_case(data[37], 'B_'), prefix_case(data[22], 'C_')
|
||||
combined = {'components': a['components']+b['components']+direct['components'],
|
||||
'connections': a['connections']+b['connections']+direct['connections']}
|
||||
base = data[37]['probes'][0]
|
||||
bbase = data[37]['probes'][-1]
|
||||
# Independent regions use different states, then all reverse pressure direction.
|
||||
states = {}
|
||||
for prefix, case, probe in [('A_', data[37], base), ('B_', data[37], bbase), ('C_', data[22], data[22]['probes'][0])]:
|
||||
states.update({prefix+key: value for key, value in zip(case['stateKeys'], probe['state'])})
|
||||
reverse = dict(states)
|
||||
for prefix in ('A_', 'B_', 'C_'):
|
||||
for field in ('m', 'U'):
|
||||
left, right = prefix+'left.'+field, prefix+'right.'+field
|
||||
reverse[left], reverse[right] = states[right], states[left]
|
||||
inputs = [states, reverse, states]
|
||||
together = compile_extended_program(reference_network(combined))
|
||||
self.assertEqual(together.evaluation_schedule['cyclicBlockCount'], 2)
|
||||
actual, counts = self.probe(together, inputs)
|
||||
together_ops = together.evaluation_schedule['operations']
|
||||
for case in (a, b, direct):
|
||||
alone = compile_extended_program(reference_network(case))
|
||||
expected, alone_counts = self.probe(alone, inputs)
|
||||
for row, wanted in zip(actual, expected):
|
||||
output_map = dict(zip((v.key for v in together.variables), row['outputs']))
|
||||
state_map = dict(zip(together.state_keys, row['rhs']))
|
||||
self.assertEqual([output_map[v.key] for v in alone.variables], wanted['outputs'])
|
||||
self.assertEqual([state_map[key] for key in alone.state_keys], wanted['rhs'])
|
||||
for i, op in enumerate(alone.evaluation_schedule['operations']):
|
||||
if op['kind'] != 'flow':
|
||||
continue
|
||||
j = next(j for j, candidate in enumerate(together_ops) if candidate['key']==op['key'])
|
||||
self.assertEqual([row[j] for row in counts], [row[i] for row in alone_counts])
|
||||
if case is direct:
|
||||
self.assertEqual([row[j] for row in counts], [1, 1, 1])
|
||||
|
||||
def test_reference_aliases_are_prepared_once_even_with_reverse_component_order(self):
|
||||
case = deepcopy(reference_data()['cases'][30])
|
||||
original = compile_extended_program(reference_network(case))
|
||||
case['components'].reverse()
|
||||
case['connections'].reverse()
|
||||
reordered = compile_extended_program(reference_network(case))
|
||||
inputs = [dict(zip(case['stateKeys'], probe['state'])) for probe in case['probes']]
|
||||
expected, _ = self.probe(original, inputs)
|
||||
actual, counts = self.probe(reordered, inputs)
|
||||
self.assertEqual(reordered.evaluation_schedule['cyclicBlockCount'], 0)
|
||||
self.assertTrue(all(value==1 for row in counts for value in row))
|
||||
for row, wanted in zip(actual, expected):
|
||||
outputs = dict(zip((v.key for v in reordered.variables), row['outputs']))
|
||||
rhs = dict(zip(reordered.state_keys, row['rhs']))
|
||||
# Permuting connection equations can change floating-point summation
|
||||
# order in the constant linear elimination, by roundoff only.
|
||||
for got, want in zip([outputs[v.key] for v in original.variables]+[rhs[key] for key in original.state_keys],
|
||||
wanted['outputs']+wanted['rhs']):
|
||||
self.assertTrue(math.isclose(got,want,rel_tol=5e-14,abs_tol=2e-12), (got,want))
|
||||
|
||||
def test_coupled_pressure_and_mixing_loop_conserves_mass_and_energy(self):
|
||||
from tests.test_native_catalog import Circuit
|
||||
b = Circuit()
|
||||
node = b.add('tee','junction')
|
||||
for i,(port,pressure,temp) in enumerate((('port_in',4e5,330),('port_out1',1e5,280),('port_out2',2e5,300))):
|
||||
chamber = b.chamber('storage'+str(i),p0=pressure,T0=temp)
|
||||
pipe = b.add('amesim_pnl00r','pipe'+str(i),diam=.01,le=1,rr=1e-5)
|
||||
b.connect(chamber,'port_1',pipe,'port_1')
|
||||
b.connect(pipe,'port_2',node,port)
|
||||
program = compile_extended_program(b.seal())
|
||||
loops = [block for block in program.evaluation_schedule['blocks'] if block['cyclic']]
|
||||
self.assertEqual(len(loops),1)
|
||||
self.assertTrue(loops[0]['pressureUnknowns'])
|
||||
self.assertIn('stream:junction',loops[0]['operations'])
|
||||
build = build_native(program)
|
||||
initial = json.loads(subprocess.run([str(build.executable),'--init'],capture_output=True,text=True,check=True,timeout=15).stdout)
|
||||
state = dict(zip(program.state_keys,initial))
|
||||
reverse = dict(state)
|
||||
equal = dict(state)
|
||||
for field in ('m','U'):
|
||||
reverse['storage0.'+field],reverse['storage1.'+field] = state['storage1.'+field],state['storage0.'+field]
|
||||
for i in (1,2): equal['storage'+str(i)+'.'+field] = state['storage0.'+field]
|
||||
rows,_ = self.probe(program,[state,reverse,equal,state])
|
||||
self.assertEqual(rows[0],rows[-1])
|
||||
for row in rows:
|
||||
rhs = dict(zip(program.state_keys,row['rhs']))
|
||||
for field in ('m','U'):
|
||||
rates = [rhs['storage'+str(i)+'.'+field] for i in range(3)]
|
||||
self.assertLessEqual(abs(sum(rates)), 1e-10 + 1e-9*sum(map(abs,rates)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user