优化雅可比矩阵计算;端口转发情况下仿真结果传输方式优化
This commit is contained in:
1 parent
3bc4be3c06
commit
aa4951b14e
28 files changed
+8038
-23
No files matched your search
@@ -0,0 +1,257 @@
|
||||
"""Compiler structure and fixed-state native probes; no integration is run."""
|
||||
from collections import defaultdict
|
||||
import ctypes
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from app.main import compile_system_xml_network
|
||||
from app.simulation.native_codegen.compiler import compile_native_program
|
||||
from app.simulation.native_codegen.extended import compile_extended_program
|
||||
from app.simulation.native_codegen.input import load_input
|
||||
from app.simulation.native_codegen.jacobian import StateDependencies
|
||||
from tests.native_reference import reference_data, reference_network
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def compile_input(path):
|
||||
_, document = load_input(ROOT / path)
|
||||
return compile_native_program(compile_system_xml_network(document))
|
||||
|
||||
|
||||
def array(program, name):
|
||||
match = re.search(r'const int '+name+r'\[\d+\] = \{([^}]*)\};', program.source)
|
||||
if not match:
|
||||
raise AssertionError(f'Missing generated {name}')
|
||||
return list(map(int, match[1].split(',')))
|
||||
|
||||
|
||||
def rows_and_colors(program):
|
||||
pointers = array(program, 'model_jacobian_col_ptr')
|
||||
indices = array(program, 'model_jacobian_row_index')
|
||||
colors = array(program, 'model_jacobian_column_color')
|
||||
rows = [set() for _ in program.state_keys]
|
||||
for column in range(len(colors)):
|
||||
for row in indices[pointers[column]:pointers[column+1]]:
|
||||
rows[row].add(column)
|
||||
return rows, colors
|
||||
|
||||
|
||||
class StructuralDependencyTests(unittest.TestCase):
|
||||
def test_unknown_reachable_values_and_calls_disable_coloring(self):
|
||||
for expression in ('mystery', 'q[900]', 'unreviewed_kernel(y[0])'):
|
||||
deps = StateDependencies(2)
|
||||
deps.expression('dy[0]', expression)
|
||||
deps.expression('dy[1]', '0')
|
||||
result = deps.build()
|
||||
self.assertFalse(result.enabled)
|
||||
self.assertIn('Unresolved', result.reason)
|
||||
deps = StateDependencies(1)
|
||||
deps.expression('w[9]', 'unused_diagnostic[0]')
|
||||
deps.expression('dy[0]', '0')
|
||||
self.assertTrue(deps.build().enabled)
|
||||
self.assertEqual(deps.build().rows, ((0,),))
|
||||
|
||||
def test_scc_closure_keeps_independent_regions_separate(self):
|
||||
deps = StateDependencies(6)
|
||||
for target, inputs in {'h[0]': ('h[1]', 'y[0]'), 'h[1]': ('h[0]', 'y[1]'),
|
||||
'h[2]': ('h[3]', 'y[3]'), 'h[3]': ('h[2]', 'y[4]')}.items():
|
||||
deps.assign(target, inputs)
|
||||
for i in range(6):
|
||||
deps.expression(f'dy[{i}]', '0')
|
||||
deps.assign('dy[2]', ('h[0]',))
|
||||
deps.assign('dy[5]', ('h[2]',))
|
||||
result = deps.build()
|
||||
self.assertTrue(result.enabled)
|
||||
self.assertEqual(result.rows[2], (0, 1, 2))
|
||||
self.assertEqual(result.rows[5], (3, 4, 5))
|
||||
self.assertEqual(result.color_count, 3)
|
||||
|
||||
def test_projection_and_stop_control_dependencies_keep_original_sources(self):
|
||||
deps = StateDependencies(6)
|
||||
deps.project_states([0, 2])
|
||||
deps.project_states([1, 3])
|
||||
for i in range(6):
|
||||
deps.expression(f'dy[{i}]', '0')
|
||||
deps.expression('g[0].p', 'y[0]+y[1]')
|
||||
deps.expression('dy[4]', 'g[0].p')
|
||||
deps.expression('dy[5]', 'y[4]')
|
||||
deps.stop_motion(4, 5)
|
||||
result = deps.build()
|
||||
self.assertEqual(result.rows[4], tuple(range(6)))
|
||||
self.assertEqual(result.rows[5], tuple(range(6)))
|
||||
|
||||
def test_catalog_patterns_have_valid_csc_and_colorings(self):
|
||||
for index, case in enumerate(reference_data()['cases']):
|
||||
with self.subTest(case=index):
|
||||
program = compile_extended_program(reference_network(case))
|
||||
info = program.jacobian_structure
|
||||
self.assertTrue(info['enabled'], info['reason'])
|
||||
self.assertTrue(info['canonicalRhs'])
|
||||
self.assertEqual(info['defaultRuntimePolicy'], info['policy'])
|
||||
self.assertEqual(info['policyScope'], 'default-runtime')
|
||||
self.assertEqual(info['verification'], '--verify-jacobian')
|
||||
self.assertNotIn('activation', info)
|
||||
rows, colors = rows_and_colors(program)
|
||||
for row, entries in enumerate(rows):
|
||||
self.assertIn(row, entries)
|
||||
self.assertEqual(len(entries), len({colors[col] for col in entries}))
|
||||
self.assertEqual(sum(map(len, rows)), info['nonzeros'])
|
||||
self.assertEqual(max(colors)+1, info['colorCount'])
|
||||
eligible = info['colorCount'] < len(program.state_keys)
|
||||
self.assertEqual(info['runtimeEligible'], eligible)
|
||||
self.assertEqual(info['runtimeFallbackReason'] is None, eligible)
|
||||
if eligible:
|
||||
self.assertEqual(info['policy'], 'CVODE colored forward differences; canonical-property-cache RHS')
|
||||
self.assertEqual(info['rhsPolicy'], 'canonical-property-cache finite differences')
|
||||
else:
|
||||
self.assertEqual(info['policy'], 'CVODE default dense differences')
|
||||
self.assertEqual(info['rhsPolicy'], 'ordinary model_eval')
|
||||
|
||||
def test_eight_branch_pattern_covers_mechanical_volume_and_uses_fewer_groups(self):
|
||||
program = compile_input('tests/data/test-mql-8-corrected.json')
|
||||
rows, colors = rows_and_colors(program)
|
||||
self.assertLess(max(colors)+1, len(program.state_keys)//2)
|
||||
self.assertTrue(program.jacobian_structure['runtimeEligible'])
|
||||
self.assertEqual(program.jacobian_structure['defaultRuntimePolicy'],
|
||||
'CVODE colored forward differences; canonical-property-cache RHS')
|
||||
self.assertIn('#define MODEL_JACOBIAN_CANONICAL_RHS 1', program.header)
|
||||
self.assertEqual(len(colors), len(program.state_keys))
|
||||
index = {key: i for i, key in enumerate(program.state_keys)}
|
||||
# PNCH012 pressure/energy includes piston volume from two moving masses;
|
||||
# a gas-only origin label would miss these position dependencies.
|
||||
target = index['amesim_pnch012_11.U']
|
||||
self.assertIn(index['amesim_mecmas21_5.x'], rows[target])
|
||||
self.assertIn(index['amesim_mecmas21_9.x'], rows[target])
|
||||
|
||||
def test_compact_path_retains_dense_fallback(self):
|
||||
program = compile_input('tests/fixtures/native-skill-test.xml')
|
||||
self.assertFalse(program.jacobian_structure['enabled'])
|
||||
self.assertFalse(program.jacobian_structure['runtimeEligible'])
|
||||
self.assertTrue(program.jacobian_structure['runtimeFallbackReason'])
|
||||
self.assertEqual(program.jacobian_structure['defaultRuntimePolicy'], 'CVODE default dense differences')
|
||||
self.assertEqual(program.jacobian_structure['rhsPolicy'], 'ordinary model_eval')
|
||||
self.assertIn('#define MODEL_JACOBIAN_COLORED 0', program.header)
|
||||
self.assertIn('#define MODEL_JACOBIAN_CANONICAL_RHS 0', program.header)
|
||||
self.assertNotIn('model_eval_jacobian', program.header)
|
||||
|
||||
|
||||
class NativeJacobianProbeTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
command = shlex.split(os.environ.get('CC', ''))
|
||||
if not command:
|
||||
compiler = shutil.which('gcc') or shutil.which('clang')
|
||||
if not compiler:
|
||||
raise unittest.SkipTest('A C compiler is required for fixed-state probes')
|
||||
command = [compiler]
|
||||
cls.compiler = command
|
||||
cls.directory = tempfile.TemporaryDirectory(prefix='native-jacobian-probe-')
|
||||
cls.addClassCleanup(cls.directory.cleanup)
|
||||
cls.root = Path(cls.directory.name)
|
||||
cls.fixture = json.loads((ROOT/'tests/fixtures/native-jacobian-cache-state.json').read_text())
|
||||
if hashlib.sha256((ROOT/cls.fixture['input']).read_bytes()).hexdigest() != cls.fixture['inputSha256']:
|
||||
raise AssertionError('Update the fixed-state fixture deliberately when changing the physical input')
|
||||
cls.program = compile_input(cls.fixture['input'])
|
||||
cls.n = len(cls.program.state_keys)
|
||||
cls.noutputs = len(cls.program.variables)
|
||||
cls.library = cls.build_library('current', cls.program.source)
|
||||
# Restore the pre-canonical gas-seeding policy for BOTH entrypoints in
|
||||
# an isolated TU. This checks ordinary dispatch/cache behavior on each
|
||||
# platform without requiring identical libm rounding across platforms.
|
||||
old = 'gas_properties=canonical?NULL:properties;'
|
||||
if cls.program.source.count(old) != 1:
|
||||
raise AssertionError('Update the explicitly restored baseline policy when changing the generator')
|
||||
baseline = cls.program.source.replace(old, 'gas_properties=properties;(void)canonical;')
|
||||
cls.baseline = cls.build_library('seeded-baseline', baseline)
|
||||
|
||||
@classmethod
|
||||
def build_library(cls, name, source):
|
||||
directory = cls.root/name
|
||||
directory.mkdir()
|
||||
(directory/'model.c').write_text(source)
|
||||
(directory/'model.h').write_text(cls.program.header)
|
||||
output = directory/('probe.dll' if os.name == 'nt' else 'probe.so')
|
||||
command = cls.compiler + ['-std=c11', '-O3', '-shared', '-Wall', '-Wextra', '-Werror',
|
||||
'-ffp-contract=off', '-fno-fast-math']
|
||||
if os.name != 'nt':
|
||||
command.append('-fPIC')
|
||||
command += ['-I', str(directory), '-I', str(ROOT/'native/include'),
|
||||
str(directory/'model.c'), str(ROOT/'native/components/kernels.c'), '-lm', '-o', str(output)]
|
||||
result = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||
if result.returncode:
|
||||
raise AssertionError(result.stderr)
|
||||
library = ctypes.CDLL(str(output))
|
||||
if os.name == 'nt':
|
||||
import _ctypes
|
||||
cls.addClassCleanup(_ctypes.FreeLibrary, library._handle)
|
||||
for name in ('model_eval', 'model_eval_jacobian'):
|
||||
function = getattr(library, name)
|
||||
function.argtypes = [ctypes.c_double, *([ctypes.POINTER(ctypes.c_double)]*3)]
|
||||
function.restype = ctypes.c_int
|
||||
return library
|
||||
|
||||
def evaluate(self, values, *, canonical=True, library=None):
|
||||
state = (ctypes.c_double*self.n)(*values)
|
||||
derivative = (ctypes.c_double*self.n)()
|
||||
outputs = (ctypes.c_double*self.noutputs)()
|
||||
function = getattr(library or self.library, 'model_eval_jacobian' if canonical else 'model_eval')
|
||||
self.assertEqual(function(self.fixture['time'], state, derivative, outputs), 1)
|
||||
self.assertEqual(list(state), values, 'RHS evaluation mutated its input state')
|
||||
return list(derivative), list(outputs)
|
||||
|
||||
def test_normal_rhs_preserves_seeded_baseline_bits(self):
|
||||
for column in (None, 71, 80, 81, 84):
|
||||
state = self.fixture['state'][:]
|
||||
if column is not None:
|
||||
state[column] += math.sqrt(2**-52)*abs(state[column])
|
||||
actual = self.evaluate(state, canonical=False)
|
||||
expected = self.evaluate(state, canonical=False, library=self.baseline)
|
||||
for got, want in zip(actual, expected):
|
||||
self.assertEqual(struct.pack(f'={len(got)}d', *got), struct.pack(f'={len(want)}d', *want))
|
||||
|
||||
def test_canonical_full_dense_and_colored_differences_agree(self):
|
||||
rows, colors = rows_and_colors(self.program)
|
||||
state = self.fixture['state'][:]
|
||||
base, _ = self.evaluate(state)
|
||||
increments = [max(math.sqrt(2**-52)*abs(value), 1e-14) for value in state]
|
||||
dense = []
|
||||
for column in range(self.n):
|
||||
trial = state[:]
|
||||
trial[column] += increments[column]
|
||||
value, _ = self.evaluate(trial)
|
||||
differences = [value[row]-base[row] for row in range(self.n)]
|
||||
for row in range(self.n):
|
||||
if column not in rows[row]:
|
||||
self.assertEqual(differences[row], 0, (row, column))
|
||||
dense.append(differences)
|
||||
groups = defaultdict(list)
|
||||
for column, color in enumerate(colors):
|
||||
groups[color].append(column)
|
||||
for columns in groups.values():
|
||||
trial = state[:]
|
||||
for column in columns:
|
||||
trial[column] += increments[column]
|
||||
value, _ = self.evaluate(trial)
|
||||
for column in columns:
|
||||
for row in range(self.n):
|
||||
if column in rows[row]:
|
||||
self.assertEqual(value[row]-base[row], dense[column][row], (row, column))
|
||||
# The historically offending cross-branch entries must be exact zeros.
|
||||
for column in (80, 81):
|
||||
for row in (68, 69, 70, 86):
|
||||
self.assertEqual(dense[column][row], 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user