"""Native catalog regression against frozen, independent Python results.""" import json import math import subprocess import unittest import numpy as np from app.simulation.native_codegen.extended import catalog_contracts, compile_extended_program from app.simulation.native_codegen.compiler import compile_native_program, NativeCapabilityError from app.simulation.native_codegen.build import build_native, toolchain from app.simulation.systems.network import SimulationNetwork from app.simulation.components.amesim.media.mediums import AmesimHeliumPengRobinsonMedium from tests.native_reference import reference_data, reference_network def has_revised_pipe_law(case): """The frozen flow values include capped pipe iterates, not checked roots. All PNL media now share the residual-checked scalar solver. Keep the frozen thermodynamic oracle; the pipe suite checks the actual resistance equation against independent bisection as well as system-level Amesim curves. """ return any(c['type'].startswith('amesim_pnl') for c in case['components']) class Circuit: def __init__(self, medium=None): self.medium=medium or AmesimHeliumPengRobinsonMedium() self.net=SimulationNetwork('native_catalog') def add(self, kind, name, **params): cls=catalog_contracts()[kind] values={p.name:p.default for p in cls.PARAMETERS};values.update(params) c=cls.create(name=name,medium=self.medium,parameters=values) self.net.add_component(c) return c def connect(self,a,ap,b,bp): self.net.connect(a.name,ap,b.name,bp) def chamber(self,name,**params): return self.add('amesim_pnch023',name,**params) def seal(self): connected={e.key for edge in self.net.connections for e in edge.endpoints} for c in list(self.net.components.values()): for p in c.active_port_definitions: if p.domain=='pneumatic' and (c.name,p.name) not in connected: cap=self.add('amesim_pnpl01','cap_'+str(len(self.net.components))) self.connect(c,p.name,cap,'port_1') return self.net class NativeCatalogTests(unittest.TestCase): @classmethod def setUpClass(cls): try: toolchain() except (OSError, RuntimeError, subprocess.SubprocessError) as exc: raise unittest.SkipTest(f'Native toolchain unavailable: {exc}') def test_frozen_python_reference(self): data = reference_data() for index, case in enumerate(data['cases']): with self.subTest(network=index): net = reference_network(case) invalid_friction = any(c['type']=='amesim_mecmas21' and c['parameters']['useFriction']==2 and c['parameters']['stoptype']!=3 and c['parameters']['fcoul']>c['parameters']['fstick'] for c in case['components']) if invalid_friction: # Three historical Python fixtures had fcoul=2, fstick=0. # Amesim itself rejects that active configuration. Preserve # the old files and verify explicit rejection in both paths. for compiler in (compile_native_program,compile_extended_program): with self.assertRaisesRegex(NativeCapabilityError,'fcoul'): compiler(net) continue # Exercise the optimized compiler as well as the general catalog. programs = [compile_extended_program(net)] if index >= len(data['cases']) - 3: programs.append(compile_native_program(net)) for program in programs: build = build_native(program) state_order = [case['stateKeys'].index(key) for key in program.state_keys] output_order = [case['outputKeys'].index(v.key) for v in program.variables] initial = json.loads(subprocess.run([str(build.executable), '--init'], capture_output=True, text=True, check=True, timeout=15).stdout) np.testing.assert_allclose(initial, [case['initialState'][i] for i in state_order], rtol=data['rtol'], atol=data['atol']) inputs = [' '.join(format(x, '.17g') for x in (probe['time'], *[probe['state'][i] for i in state_order])) for probe in case['probes']] result = subprocess.run([str(build.executable), '--probe'], input='\n'.join(inputs)+'\n', capture_output=True, text=True, check=True, timeout=30) rows = [json.loads(line) for line in result.stdout.splitlines()] self.assertEqual(len(rows), len(inputs)) for row, probe in zip(rows, case['probes']): self.assertTrue(row['success']) if not has_revised_pipe_law(case): expected_rhs=list(probe['rhs']);expected_outputs=list(probe['outputs']) # The frozen Python model incorrectly applied dry # friction in restitution mode. Keep its bytes, but # use Newton's law as the oracle for those two fields. old=dict(zip(case['outputKeys'],probe['outputs'])) for component in case['components']: if component['type']!='amesim_mecmas21':continue params=component['parameters'];name=component['name'] if params['stoptype']!=3 or params['useFriction']!=2 or params['fcoul']==0:continue velocity=old[name+'.v'];mass=params['mass'] self.assertTrue(params['xmin']