"""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()